Run policy
Every field of policy_config: how a run checks its work, what it may spend, which harness it uses, and when it waits for you.
policy_config is a JSON object you can send on POST /runs. It sets how one run works. Leave out a key and the run uses its default. An unknown key fails with INVALID_POLICY (HTTP 400).
You can also save defaults so you don't send the same policy on every run. See Saved defaults.
Top-level fields
| field | type | default | what it sets |
|---|---|---|---|
validation | object | see below | how the run checks its change |
reviews | object | see below | which review gates the run must pass |
budget | object | see below | repair tries and the spend limit |
egress | object | managed_default | network access for the sandbox |
artifacts | object | standard | how long evidence files are kept |
harness | object | none | which coding agent and model run the change |
subscription_usage | object | see below | use of a subscription past its usage limit |
usage_failover | object | see below | what runs when your subscriptions are used up |
tools | object | none | built-in tools and your own tools for the run |
autonomy | enum | auto | what happens once the change passes its checks |
spec_review | enum | none | if the run waits for you to approve its plan |
unattended | bool | false | never pause the run to ask a question |
context_research_enabled | bool | true | a short web search for current practice before the plan |
auth_mode | enum | api_key | who pays for the AI: credits or a subscription |
subscription_credential_id | UUID | none | the subscription a run uses |
subscription_provider | enum | none | claude_max or chatgpt |
validation
| field | type | default | what it does |
|---|---|---|---|
profile | enum | auto | A stack hint: auto, python, nextjs, vite, flutter, react_native, native_ios or native_android. Syndai still detects stacks from your files. A hint only adds to what it finds. |
level | enum | standard | smoke, standard or strict. See Validation. |
base_freshness | enum | as_of_base | latest_main counts CI only when the pull request sits on the current tip of the base branch. With as_of_base, CI still counts after the base branch moves, as long as it did not move in a file the change touches. |
bootstrap_when_toolless | bool | false | true: when your repo has no checks Syndai can run, the agent adds a small test setup so the change can be checked. false: the run stops for lack of checks. |
template_trees | list of strings | [] | Folders that hold templates your repo ships, not code it builds. Syndai never builds or tests inside them. Each entry is a plain path like tools/scaffold. No leading or trailing slash, no . or .., at most 50. |
inputs | map of string to list | {} | What each command folder reads, as path prefixes: {"api": ["api/", "shared/"]}. . is the whole repo. A check is skipped when the change touches none of its inputs. A passing check is reused when its inputs, command and setup did not change. No globs. See syndai.yaml. |
timeout_seconds | int | 600 | Time limit for a check that runs on GitHub Actions and has no limit of its own. More than zero. |
github_poll_attempts | int | 60 | How many times Syndai polls a GitHub Actions check. More than zero. |
github_poll_interval_seconds | number | 5.0 | Seconds between those polls. At least 2.0. |
You never send commands. Syndai finds them in your repo. These keys are refused with INVALID_POLICY: commands, execution_environment, github_workflow_id, github_result_artifact_name and artifact_upload.
reviews
| field | type | default | what it does |
|---|---|---|---|
required_gates | list | [] | Gates the run must pass. Allowed: planning_review, adversarial_review, security_review and preflight. |
hitl_on_exhaustion | bool | true | When the run uses up its repair tries, it waits for you instead of failing. A run with no one to answer never waits. false turns this off. |
Every run passes security_review, preflight and planning_review, whatever you send. So adversarial_review is the one gate you can add. Names that act, such as merge, push, deploy or create_pr, are refused. See Review gates.
budget
| field | type | default | what it does |
|---|---|---|---|
retry_budget | int | 3 | How many times the run may fix a failed check before it stops. 0 means no repair tries. |
max_budget_usd | number | none | The most this run may spend, in US dollars. At least 5. |
branch_strategy | enum | branch_only_never_merge | branch_only_never_merge or branch_then_pr. Neither one lands a change. Landing is set by autonomy. |
egress
| field | type | default | what it does |
|---|---|---|---|
profile | enum | managed_default | managed_default gives the sandbox normal network access. offline blocks all outbound traffic. |
An offline run can't call your own MCP servers, since they need the network.
artifacts
| field | type | default | what it does |
|---|---|---|---|
retention_profile | enum | standard | How long evidence files (screenshots, logs, reports) are kept. standard is up to 30 days and extended is up to 90. pinned keeps them until you delete them. |
harness
| field | type | default | what it does |
|---|---|---|---|
kind | enum | none | The coding agent: pi, opencode, claude_code or codex_cli. |
model | string | none | The model for that agent. It must be on the agent's allowed list. The error lists the allowed models. |
effort | enum | none | low, medium, high, xhigh or max. |
model needs kind. On credits (auth_mode: api_key) you can pick pi, opencode or claude_code. On a subscription you can pick claude_code (Claude Max) or codex_cli (ChatGPT). Leave out auth_mode and Syndai picks it for you. claude_code uses your Claude subscription when you connected one, or credits when you did not. codex_cli needs a connected ChatGPT subscription.
subscription_usage
| field | type | default | what it does |
|---|---|---|---|
allow_extra_usage | bool | false | true lets a run use your subscription even when its usage is near the limit or can't be read. Your provider may then charge for extra usage. |
usage_failover
| field | type | default | what it does |
|---|---|---|---|
backup_chain | list or null | null | What runs when every subscription you connected is used up. null uses the default: your other subscription if it has room, then pi. [] means no backup: the run waits and goes on when a usage window resets. A list is the agents to try in order, from pi, opencode, claude_code and codex_cli. At most 3, no repeats. |
api_funding | enum or null | null | Who pays when a subscription run falls back to an API agent. credits pays from workspace credits, never from your own OpenRouter key. null and workspace_default use the workspace's normal lane. |
tools
| field | type | default | what it does |
|---|---|---|---|
builtins | enum | all | all gives the run Syndai's built-in tools. none gives it none of them. Your own tools still work. |
disabled_builtin_capabilities | list | [] | Built-in tools to take away from all. Ignored with none. At most 100, no repeats. |
custom_tool_ids | list or null | null | Your own tools (skills and MCP servers) for this run, as UUIDs. null gives the run every tool you turned on. A list, even an empty one, gives it only those. At most 50, no repeats. |
Leave out tools and the run gets all built-in tools and every tool you turned on. The run calls your MCP servers through Syndai. Your server's URL and secret never enter the sandbox.
autonomy
What happens once the change passes its checks. Values: auto, gated, merge and push.
autoopens the pull request.gatedstops before the pull request. You see the change and pick Approve to open it, or Request changes to send notes for another try.mergeopens the pull request, then squash merges it when every gate is green. It holds if a gate fails. The security review and preflight must pass. The base branch must not have moved in a file the change touches. CI must pass too.pushruns the same gates. Then it moves the default branch up to the checked commit. If the base branch moved, it squash merges the pull request.pushis for the API only.
A held change waits on the run with Merge anyway, Retry and Close. Only a person signed in at app.syndai.ai can pick Merge anyway. A key gets HUMAN_APPROVAL_REQUIRED. A per-run merge or push counts only when it comes from an API key, an owner or admin, or a user's own run. Some runs come from a request someone else wrote, like a site visitor, a member or a Sentry alert. Those always hold before they land.
spec_review
If the run waits for you to approve its plan before it builds. Values: auto, timed and hold.
- Not set: the run builds at once. A large plan waits like
timed. autonever waits, even for a large plan.timedwaits for you, then builds on its own if you don't answer in time.holdwaits until you approve. It never builds on its own. If no one answers, the run ends without building.
A run with no one to answer never waits. That covers scheduled runs and unattended runs.
unattended
true means no one will answer questions. When the agent is unsure, it goes on with its best guess and says so in the pull request. It does not pause. Pauses for a sign-in or a usage limit still happen.
context_research_enabled
true (the default) runs a short web search for current best practice before the plan. The search is built from your task text, with known secret patterns taken out first. Send false if your task text must never reach the search provider.
Subscriptions
auth_mode:api_key(the default) orsubscription.subscription_credential_id: the subscription you connected.subscription_provider:claude_maxorchatgpt. It must match the harness:claude_maxgoes withclaude_code, andchatgptgoes withcodex_cli.
Send subscription_credential_id and subscription_provider only with auth_mode: subscription. A subscription agent needs one of the two.
Rules
A policy that breaks one of these fails with INVALID_POLICY (HTTP 400). detail names the field.
- No unknown keys, at any depth.
- These keys are refused by name:
webhook_url,mobile_validation,web_validation,backend_validation,policy_config_override,internet_access,network_allow_list,allow_hostsandallow_cidrs. Set up webhooks with the webhooks API instead. timeout_secondsandgithub_poll_attemptsare more than zero.github_poll_interval_secondsis at least2.0.retry_budgetis zero or more.max_budget_usdis at least5.- The harness, model and subscription fields agree, as above.
A null value clears the key back to its default.
Saved defaults
Save a policy once and every new run starts from it:
GET /policy/schemareturns this schema as JSON Schema.PUT /policy/defaultssaves a default. Addcoding_repository_idto save one for a single project.GET /policy/defaultsreads the current default, andGET /policy/defaults/historylists past ones.
Layers apply in this order, and the last one wins for each key:
- the workspace default
- the project default
- the repo's syndai.yaml
- the run's own
policy_config
syndai.yaml is read after the run's sandbox starts. So it can't change egress.profile, tools.builtins, tools.disabled_builtin_capabilities, budget.branch_strategy, budget.max_budget_usd or autonomy. Set those in a saved default or on the run.
A new workspace starts with this default: spec_review: auto, autonomy: merge, unattended: true, and usage_failover set to backup_chain: null and api_funding: credits. It is an ordinary saved default, so you can change it.