`syndai.yaml` is a file you commit to your repo. It sets policy for every run on that repo, so you do not have to send the same `policy_config` each time. You do not need it to use the API.

## Where it lives

Syndai looks for the file in two places, in this order:

1. `syndai.yaml` at the repo root
2. `.syndai/workflow.yaml`

It reads only the first one it finds. The file can be at most 64 KB.

Syndai reads it from the repo as checked out when the run starts, before the agent changes anything. So a run that edits `syndai.yaml` does not change its own policy. The next run reads the new file once it is merged.

A run that fixes CI on an open pull request starts on that pull request's branch. That branch holds an earlier run's changes, so Syndai reads `syndai.yaml` from the pull request's base branch instead.

## What wins

When the same key is set in more than one place, the one further right wins:

```
Syndai defaults < saved defaults < syndai.yaml < policy_config on the run
```

Saved defaults are the ones you set for the workspace or a project, in the app or with `PUT /policy/defaults`. Keys you leave out of the file keep the value from the layer below.

A few keys keep the value the run already had, whatever the file says. The run's sandbox is set up before the file is read. Anyone who can merge a change can also edit the file. So these keys are not taken from it:

- `egress.profile`
- `budget.branch_strategy`
- `budget.max_budget_usd`
- `autonomy`
- `tools.builtins` and `tools.disabled_builtin_capabilities`, once a layer below has set them

Set these on the run, or in your saved defaults.

## An example

This repo's own file is a real one. Here is a smaller example:

```yaml
# syndai.yaml
version: 1

default_policy:
  validation:
    level: standard          # smoke, standard or strict
    timeout_seconds: 600     # cap for each check command, in seconds

    # Folders that hold templates you ship elsewhere.
    # Syndai never builds or tests inside them.
    template_trees:
      - tools/scaffold

    # What each check folder reads. See "validation.inputs" below.
    inputs:
      api:
        - api/
        - shared/
        - Makefile
      apps/web:
        - apps/web/
        - packages/
        - package.json
        - pnpm-lock.yaml

  reviews:
    required_gates:
      - adversarial_review

  budget:
    retry_budget: 3          # how many times a failed check may be fixed and run again

  artifacts:
    retention_profile: standard   # standard, extended or pinned
```

## Top-level keys

| key | what it does |
|---|---|
| `version` | must be `1`; you can leave it out |
| `default_policy` | policy for every run on this repo, in the same shape as `policy_config` |
| `validation` | a short form; it is merged into `default_policy.validation` |
| `review_gates` | a short form; each name is added to `default_policy.reviews.required_gates` |
| `test_login` | how checks sign in to your app; see [Test logins](/docs/test-login) |

Any other top-level key makes the file invalid.

### `default_policy`

This takes the same fields as `policy_config` on a run. [Policy](/docs/api/policy) lists every field. The ones people set here most:

| key | values |
|---|---|
| `validation.profile` | `auto`, `python`, `nextjs`, `vite`, `flutter`, `react_native`, `native_ios`, `native_android` |
| `validation.level` | `smoke`, `standard`, `strict` |
| `validation.base_freshness` | `as_of_base`, `latest_main` |
| `validation.timeout_seconds` | a whole number above zero |
| `validation.template_trees` | a list of folders; see below |
| `validation.inputs` | a map of folder to paths; see below |
| `validation.owner_checks` | checks only you can make pass; see below |
| `reviews.required_gates` | gates to add; see [Review gates](/docs/api/review-gates) |
| `budget.retry_budget` | a whole number, zero or more |
| `artifacts.retention_profile` | `standard`, `extended`, `pinned` |

To turn off the agent's web tools, set `tools.disabled_builtin_capabilities: [coding.read.web_search, coding.read.web_fetch]` in a saved default or on the run. That also turns off the agent's own built-in web search and fetch. It does not cut the sandbox's network: under `managed_default` egress a shell command can still reach the internet. To keep task text inside the sandbox, also set `egress.profile: offline` in a saved default or on the run. `context_research_enabled` is retired and does nothing: Syndai ignores it here and on a run.

Some validation keys are never accepted, here or on a run. These are `commands`, `execution_environment`, `github_workflow_id`, `github_result_artifact_name` and `artifact_upload`. Syndai works out the check commands itself. See [Validation](/docs/api/validation).

### `validation.template_trees`

These are folders of files you ship as data, like starter kits you copy into other repos. They are not parts of this repo. Syndai never plans builds or tests inside them.

Each entry is a plain path from the repo root, like `tools/scaffold`. It has no slash at the start or end, and no `.` or `..` part. You can list up to 50, with no repeats.

### `validation.inputs`

This lists every path that a folder's checks read. Use it when a folder's checks read files outside that folder. Two common cases: a backend test suite that loads a shared schema, or a web app that builds from shared packages.

```yaml
default_policy:
  validation:
    inputs:
      api: [api/, shared/, Makefile]
```

How it works:

- **Keys** are the folders the checks run in, as Syndai's check plan names them, like `api` or `apps/web`. Use `.` for the repo root.
- **Values** are paths from the repo root. They match whole path parts. So `shared/` covers `shared/x.ts`, and `Makefile` covers only `Makefile`. A `/` at the end is optional. `.` means the whole repo.
- No wildcards. The characters `*`, `?`, `[`, `]`, `\` and `:` are refused.
- Each list needs 1 to 50 entries, with no repeats. You can list up to 50 folders.
- The folder itself always counts as an input, even if you leave it out.

What it changes:

- **Skipping.** A check runs only when the run's change touches one of its inputs. With no entry for a folder, its checks run when the change touches that folder. They also run when it touches a root manifest or lock file, or anything under `packages/`.
- **Reuse.** A check may have passed earlier in the run. Later, its inputs, command and environment may all be the same. Then Syndai keeps that pass and does not run the command again. With no entry for a folder, a pass is kept only when the whole repo is unchanged.

List every path a folder's checks read. If you leave one out, it can change and the check will not run again.

### `validation.owner_checks`

These are checks that only you can make pass. An example is a check that compares a saved result from a paid eval with the code it tested. Syndai's sandbox cannot run that eval, so the agent could never fix the check. It could only fake the saved result.

```yaml
default_policy:
  validation:
    owner_checks:
      - check: check-step/check-eval-result
        step: make eval (paid), then commit evals/result.json
        paths: [evals/result.json]
```

Each entry has:

- **`check`**: the check's name in Syndai's check plan, or a make target. Use a make target that `make check` runs as its own target, not one inside a script that runs other steps.
- **`step`**: one line saying what you run to make it pass. Syndai shows it to you word for word.
- **`paths`** (optional): the files your step writes, as plain paths from the repo root.

What it changes:

- **In the run.** When the check fails, and nothing else fails with it, Syndai doesn't ask the agent to fix it. The run still opens its pull request. The pull request says the check needs your step.
- **In CI.** A CI failure on only this check doesn't start a "Fix CI" run or offer one. It also doesn't count as passing.
- **Landing.** Syndai never merges such a change itself. It waits for you when the check failed, or when the change edits one of the `paths`. Run the step, push the result to the pull request's branch, then merge the pull request yourself. "Merge anyway" doesn't skip this hold.

Syndai matches a check by its name. For make targets, it matches only when it re-runs your CI's make command with `-k`, which keeps make going after a failure. Make then prints a line for each target that failed, so Syndai can tell when only your check failed.

You can list up to 20 checks, with no repeats. Each `step` is one line of at most 300 characters. Each check can list up to 20 `paths`.

### `review_gates`

A short way to add gates:

```yaml
review_gates:
  - adversarial_review
```

Each name is added to `reviews.required_gates`. The gates `planning_review`, `security_review` and `preflight` are always required, with or without this list.

## When the file is broken

The run fails before the agent starts if the file:

- is not valid YAML, or its top level is not a map;
- is larger than 64 KB;
- has a `version` other than `1`;
- has a top-level key not listed above.

The run's failure reason is `invalid_policy`, and the message names the file and the problem.

## What it cannot do

- Turn off the three required review gates.
- Change the network profile, branch strategy, spend cap or `autonomy` of a run.
- Set check commands or a GitHub workflow to run.
- Pick webhook events. Each endpoint picks its own with `event_types`; see [Webhooks](/docs/api/webhooks).
