Migrating and duplicating content policies
Use the content policy migration tool to copy a policy—and its evaluation dataset and trained model, when applicable—from one DynamoAI environment to another, or to duplicate a policy within the same environment.
The tool runs in three modes:
| Mode | Purpose |
|---|---|
| export | Download policy metadata, dataset, and model info from the source API into a local folder (default ./policy_data, or --output-dir) |
| import | Read that local folder and create or update a policy in the target environment from the saved JSON files |
| update-trained | Attach model weights at an S3 path so an imported policy is in a trained state (uses the target API only; no local export folder required) |
For custom-trained policies, import with --import-model already performs the update-trained step. You do not run a separate third step in that case.
Files written by export (and read by import)
Export creates up to three JSON files under your output directory. Import expects the same files to be present (except finetuned_model.json is only required when using --import-model).
| File | Created when | Contents |
|---|---|---|
policy.json | Always | Policy definition: name, description, allowed/disallowed behaviors, moderation method, action, applicable direction (input/output), and related settings |
dataset.json | Always | Guardrail evaluation dataset: datapoints (prompts, compliance labels, review state), plus dataset metadata such as version and behavior lists |
finetuned_model.json | Always (written on every export) | Latest finetuned-model metadata from the source. If the policy has no trained model yet, export saves {}. When trained, includes fineTunedModelZipS3Path and isModelEncrypted. Required for import only when using --import-model |
Note: Export does not download the weight archive itself—only the S3 path and metadata. Weights remain in object storage; import/--import-model or update-trained registers that path (or a new path you provide) on the target policy.
Example layout after export:
policy_data/
├── policy.json
├── dataset.json
└── finetuned_model.json # {} if policy is not trained yet
Use a dedicated folder per policy (recommended), for example ./policy_data/export_my_policy_<id>/, and pass the same path to import via --output-dir.
On export, the script fetches the full evaluation dataset with pagination (500 datapoints per page) and writes all three files. On import, it reads policy.json and dataset.json from --output-dir; it reads finetuned_model.json only when you pass --import-model.
Tool setup
Download the migration tool
The script calls the platform API, so use the copy that matches the platform version of the environments you are migrating between. When source and target run different versions, use the target's. If you are not sure which version an environment runs, contact support before migrating.
Download the three files for your version into a working directory (for example content-policy-migration/):
| Platform version | Files |
|---|---|
| 3.26 (latest) | script.py · requirements.txt · .env.example |
| 3.25 | script.py · requirements.txt · .env.example |
- script.py: the migration script (
export,import,update-trained) - requirements.txt: Python dependencies (
requests,python-dotenv) - .env.example: template for API URLs and keys
Older releases do not support every option on this page. For example, --target-policy-id and --default-policy are not in the 3.25 script. Run python script.py --help to see what your copy supports.
Requirements
- Python 3.6 or later
- API access to the source and target environments:
- Source: viewer access (or higher) to the policy you are copying
- Target: DynamoGuard editor access (or higher) to create or update policies
- URLs and API keys for both environments (they can be the same when duplicating within one deployment)
The script requires a .env file in the same directory as script.py. If .env is missing or any of the four variables below are unset, it exits before running.
Install dependencies
From the migration tool directory:
pip install -r requirements.txt
Configure credentials
Copy .env.example to .env and set values for the source and target environments:
SOURCE_VPC_URL=https://api.your-source.example
SOURCE_API_KEY=...
TARGET_VPC_URL=https://api.your-target.example
TARGET_API_KEY=...
When source and target are the same deployment, point both SOURCE_* and TARGET_* variables at that environment.
Quick reference: how many steps?
| Scenario | Steps |
|---|---|
| Duplicate or copy within the same environment (custom-trained) | export → import --import-model |
| Copy to another environment | copy weights to target storage → export → import (no --import-model) → update-trained (target S3 path) |
| Copy to another environment whose compute role already reads the source bucket | export → import --import-model |
| Default / out-of-the-box (OOB) policies | export → import --default-policy (no model transfer) |
Whether the target can read the source bucket is a storage permission held by the target's compute role. The migration tool does not check it. If you do not know the answer, treat the environments as isolated and copy the weights. The extra copy is cheaper than the recovery.
Duplicating a policy in the same environment
When source and target are the same deployment, point both SOURCE_* and TARGET_* variables at that environment (and use an API key with the permissions above).
Model weights already live in that environment’s storage, so you can finish in two commands:
# 1. Export the policy you want to copy
python script.py --mode export --policy-id <POLICY_ID> \
--output-dir ./policy_data/my_policy_export
# 2. Import as a new policy, including trained weights
python script.py --mode import --import-model \
--output-dir ./policy_data/my_policy_export
The result is a new policy with the same configuration, dataset, and trained model.
Updating an existing policy (new version)
To refresh an existing policy instead of creating a duplicate, pass the target policy ID on import:
python script.py --mode import --import-model \
--target-policy-id <EXISTING_POLICY_ID> \
--output-dir ./policy_data/my_policy_export
This updates behaviors and description, syncs the dataset, and restores the trained state when --import-model is used.
With --target-policy-id, import does not create a new policy. It:
- Updates the guardrail definition (name, description, allowed/disallowed behaviors) via the target API.
- Incrementally syncs datapoints: unchanged points are skipped; only adds and deletes needed to match the export are applied (fewer unnecessary policy versions).
What import does (new policy vs. versioning)
| Import path | Behavior |
|---|---|
New policy (no --target-policy-id) | Creates a policy and guardrail dataset on the target, sets the dataset stage to ready to train (approveReady), and uploads datapoints in batches of 500. For custom policies, methodParams uses a placeholder adapter path until --import-model or update-trained sets real weights. For OOB policies, use --default-policy to keep the original methodParams (adapter path). |
Versioning (--target-policy-id) | Updates config and incrementally syncs datapoints on the existing policy ID (see above). |
--import-model (either path) | After import, registers the S3 path and encryption flag from finetuned_model.json and marks the policy trained (same logic as update-trained). |
update-trained (standalone) starts and cancels a training job on the target policy, then patches finetuned-model metadata with your S3 path and marks training complete. Use this when import ran without --import-model.
The cancel step needs at least one guardrail diff in a training state. A policy that was just imported, or that has no unapplied changes, has none, and the command fails partway with No diffs in training status. The start step has already run at that point, so a training job is left queued. See Troubleshooting.
Copying between separate environments
Policy metadata and dataset
Always start with export on the source, then import on the target:
python script.py --mode export --policy-id <SOURCE_POLICY_ID> \
--output-dir ./policy_data/export_<name>
python script.py --mode import --output-dir ./policy_data/export_<name>
Without --import-model, the new policy is ready to train—configuration and datapoints are in place, but no trained weights are attached yet.
Model weights (custom-trained policies only)
Custom policies store finetuned weights in object storage (S3). Export saves the weight location in finetuned_model.json (fineTunedModelZipS3Path).
Same environment — You do not need to copy weights. Use --import-model on import.
Separate environments — Choose one approach:
-
Shared or cross-account access — Use this only once you have confirmed that the target's compute role can read the source bucket and path. Import with
--import-modelthen registers the exported S3 path on the target policy. -
Isolated environments (recommended) — Copy the weight archive into storage that belongs to the target account (or a bucket the target can read) before you import. Then either:
- Run import without
--import-model, then update-trained with the new path, or - Edit
finetuned_model.jsonto the new path before import with--import-model.
Do not import with
--import-modelwhilefinetuned_model.jsonstill holds the source path. The import succeeds and the policy reports a trained state. The mistake stays invisible until the target pulls the weights and the deployment fails. Recovering from that state needs support involvement. - Run import without
# Import dataset and config only
python script.py --mode import --output-dir ./policy_data/export_<name>
# Attach weights from the target environment's S3 path
python script.py --mode update-trained \
--policy-id <TARGET_POLICY_ID> \
--model-weights s3://target-bucket/path/to/weights.zip \
[--is-encrypted]
Use --is-encrypted if the weights file is encrypted.
Important: Do not run update-trained after import if you already used
--import-modelon that import. That flag runs the same trained-state update automatically.
When you use --import-model, the tool also sets whether the weights are encrypted from the source policy metadata saved at export time (isModelEncrypted in finetuned_model.json). You do not need (and cannot use) --is-encrypted on import — only on update-trained when you attach weights manually.
Default (out-of-the-box) policies
Built-in policies (for example Safety or Prompt Injection) use pre-installed model adapters in the moderation service. They do not have separate finetuned weight files to copy.
python script.py --mode export --policy-id <POLICY_ID> \
--output-dir ./policy_data/export_oob
python script.py --mode import --default-policy \
--output-dir ./policy_data/export_oob
- Use
--default-policyso adapter settings are preserved. - Do not use
--import-modelor update-trained for OOB policies.
The target environment must have the corresponding default adapters deployed for inference to work.
Verify the migration
A clean command run does not mean the policy is serving. Import and update-trained write a trained state to the target's database. Loading the weights from object storage is a separate step. A policy can report training complete while its model never loads.
Check all three policy statuses on the target before you treat the migration as done:
| Status field | Expected after migration |
|---|---|
| Training | Training Complete |
| Stage | Ready to Apply |
| Deployment | Deployed |
Deployment is the field that confirms the weights loaded. A policy that shows Training Complete but Not Deployed has a weight path its environment cannot read. See Troubleshooting.
Then test the policy against sample inputs, as you would a policy you had just created.
Troubleshooting
Imported policy reports trained, but the guardrail never serves
Symptom. Import with --import-model succeeds and the policy reports a trained state, but its Deployment status stays Not Deployed. The model deployment restarts repeatedly, and its logs show a storage permission error:
ClientError: An error occurred (403) when calling the HeadObject operation: Forbidden
AccessDenied ... is not authorized to perform: s3:ListBucket
on resource: "arn:aws:s3:::<source-bucket>"
Cause. --import-model registers the fineTunedModelZipS3Path from finetuned_model.json exactly as exported, and that path belongs to the source environment. Nothing checks that the target can read it, and the import reports success. The failure surfaces only when the target pulls the weights.
Avoid it. Copy the weights into target-readable storage before you import, then import without --import-model and attach the new path with update-trained.
update-trained fails with "No diffs in training status"
Symptom.
Started training.
HTTP Error: 400 Client Error: Bad Request for url: .../cancelTraining
Response: {"message":"No diffs in training status for policyId: <id>", ...}
Cause. update-trained starts a training job and then cancels it. The cancel step requires at least one guardrail diff in a training state. A freshly imported policy, or any policy with no unapplied changes, has none, so the cancel is rejected.
What it leaves behind. The start step already succeeded, so the policy now holds a queued training job and a new version. The script exits before patching the S3 path, and the weight path is unchanged. If a worker later picks up that queued job, it trains a new model from the target environment's own dataset and deploys it. That replaces the weights you intended to migrate.
Recovery. Contact support with the policy ID. Do not re-run update-trained. Each run queues another training job.
Command reference
Export
python script.py --mode export --policy-id <SOURCE_POLICY_ID> [--output-dir ./policy_data]
| Argument | Required | Description |
|---|---|---|
--policy-id | Yes | Source policy ID to export |
--output-dir | No | Local folder for JSON files (default: ./policy_data) |
Writes JSON files to the output directory. See Files written by export for details.
Import
python script.py --mode import [--output-dir ./policy_data] [--import-model]
python script.py --mode import --target-policy-id <TARGET_POLICY_ID> [--import-model]
| Flag | When to use |
|---|---|
--output-dir | Folder containing exported JSON (default: ./policy_data) |
--import-model | Target can use the exported S3 weight path; policy ends trained; isModelEncrypted copied from export (no --is-encrypted) |
--target-policy-id | Update an existing policy (new version) instead of creating a new one |
--default-policy | OOB / default policies only; preserves original methodParams |
Update-trained (step 3 only)
python script.py --mode update-trained \
--policy-id <TARGET_POLICY_ID> \
--model-weights <S3_PATH> \
[--is-encrypted]
| Argument | Required | Description |
|---|---|---|
--policy-id | Yes | Target policy ID (after import) |
--model-weights | Yes | S3 URI of the weight archive the target can read |
--is-encrypted | No | Set if the weights file is encrypted |
Use only when import was run without --import-model and you have a weight path the target environment can access. Does not read --output-dir.