---
title: "Config as code"
description: "Keep an environment's actions, packs, approval policies, impact limits and budgets in anlyon.yaml. anlyon plan shows each change as tightening or loosening, anlyon apply applies it, anlyon pull writes what is live."
icon: "file-code"
---

`anlyon.yaml` holds what your agent may do in each environment: its actions, the packs installed, the approval policies, the impact limits and the budgets. You change it in a pull request like any other code. `anlyon plan` shows the difference with what is live, `anlyon apply` makes it live, and `anlyon pull` writes what is live back to the file.

## Know why you would use it

Guardrails changed by hand in a console drift: nobody remembers why a limit was raised, staging and production stop matching, and a change that loosens a rule ships with no review.

- **Less risk.** Every change is marked **tightening** or **loosening** before it applies. Tightening applies at once. Loosening, from a CLI login or an API key, waits for a workspace admin to approve it at a sign-off link: your agent, or the CI job it can trigger, can never loosen its own guardrails.
- **Less effort.** One file per repository, every environment in it. Review the plan in the pull request, apply on merge.
- **A record.** The file's history says who changed which rule, when and why.

## Start from what is live

```bash
anlyon auth login --scope anlyon:guardrails
anlyon pull
```

`pull` writes the environment your credential is bound to into `anlyon.yaml`, creating the file if there is none. Other environments in the file and its comments are kept. Commit the file.

The **Guardrails** consent group lets a CLI login read and change policies and limits. It starts unticked on the consent screen. In CI, use an API key with `policies:read`, `policies:write`, `impact-limits:write`, `actions:write`, and `budgets:read` and `budgets:write` if the file has budgets.

## The file

```yaml
# anlyon.yaml
version: 1
environments:
  production:                       # an environment's slug
    secrets: [POSTMARK, GITHUB]     # names that must exist. Never values.
    packs:
      github: { actions: [list-issues, create-issue], withLimits: true }
    actions:
      send-receipt:
        description: Email a receipt
        method: POST
        url: https://api.postmarkapp.com/email
        headers: { X-Postmark-Server-Token: "{{secret:POSTMARK}}" }
        governed: true
        impact: { dimension: emails, amount: count(input.to), bound: exact }
    policies:
      big-refunds:
        name: Refunds over $500 need two approvers
        target: { action: refund }
        when: input.amount >= 50000
        approvals: 2
    limits:
      Emails per day: { dimension: emails, amount: 500, period: day }
      Old limit: null               # archive it
    budgets:
      <api-key-id>: { monthlyCostCentsCap: 5000, dimensionCaps: { actionInvokes: 10000 } }
  staging:
    limits:
      Emails per day: { dimension: emails, amount: 50, period: day }
```

| Section | Holds | When it is not in the file |
| --- | --- | --- |
| `secrets` | Secret names the actions need. `plan` lists a missing one as **needs you**: store its value with [`anlyon connections add`](/cli/commands#anlyon-connections-add-integration) or in the console. | Not checked. |
| `packs` | Catalogue packs by name (`github`) or a pack directory, with the [pack flags](/cli/commands#install-a-pack): `actions`, `withLimits`, `secretNames`, `params`, `credential`, `noPrefix`. | Installed packs are left alone. |
| `actions` | Actions by name: `description`, `method`, `url`, `headers`, `body`, `inputSchema`, `requiresApproval`, `enabled`, `governed`, `impact`, `verify`. A field you leave out is not managed. | Left alone. |
| `policies` | The `policies` block of a [policy file](/trust-control/policy-as-code). The file owns them: removing one from the file deletes it. | Left alone. |
| `limits` | Impact limits by name: `dimension`, `amount`, `period`, and `currency`, `action`, `resourcePrefix`, `scope` (`environment` or `end_user`). `null` archives one. | Left alone. |
| `budgets` | Budgets by the API key id they cap: `monthlyCostCentsCap`, `dimensionCaps`, `alertThreshold`, `enabled`. `null` removes one. | Left alone. |

A credential acts on the environment it is bound to, so `plan` and `apply` use that environment's entry. Policies are stored under the policy file id `anlyon.yaml`; set `policyFile:` at the top to use another.

**Secret values never go in the file.** A value that looks like a credential (an `Authorization` header that does not reference `{{secret:NAME}}`, or a string shaped like a provider key) stops the command and names where it is.

## Plan

```bash
anlyon plan
```

```text
Plan for anlyon.yaml: environment production (<environment-id>)

  ! secrets.GITHUB                 needs you   store its value: anlyon connections add, or Secrets in the console. The file holds names only.
  ~ actions.send-receipt           tightening  adds an impact declaration
  + policies.big-refunds           tightening  adds a policy that gates calls
  ~ limits.Emails per day          tightening  lowers the limit from 1000 to 500
  - limits.Old limit               loosening   archives the limit (10 emails per day)

3 tightening, 1 loosening (waits for a person's sign-off), 0 neutral, 1 needs you, 4 unchanged.
```

`plan` sends no change. `--markdown` prints a table for a pull-request comment; `--json` prints `{data: {changes, summary, errors}}`.

How each change is read:

| Change | Direction |
| --- | --- |
| Add an action, enable one, change where or what it sends, remove an impact or a read-back | loosening |
| Disable an action, require approval, make it governed, add an impact or a read-back | tightening |
| Add a policy that gates or denies; make one stricter | tightening |
| Delete or disable a policy that gates calls; make one looser; change what it matches | loosening |
| Add a limit, or lower one | tightening |
| Raise, archive or replace a limit | loosening |
| Add or lower a budget cap, turn a budget on, alert earlier | tightening |
| Raise or remove a cap, turn a budget off, alert later | loosening |
| A description, a rename, a disabled policy | neutral |

The server makes the final call when you apply. A change it reads differently is reported by `apply`.

## Apply

```bash
anlyon apply --yes
```

`apply` plans, then makes the changes: tightening and neutral ones at once, loosening ones last. Policies go in two passes, so a new rule that tightens is never held up behind one that loosens.

A loosening change made with a CLI login or an API key is not applied. The server answers with a sign-off request, and `apply` prints its link:

```text
These changes loosen a guardrail. A workspace admin approves each one at its link; nothing here can approve them:
  limits.Old limit: needs a person's sign-off
    https://console.anlyon.com/sign-offs/sor_...

  limits.Emails per day  applied
  limits.Old limit  sign-off pending (sor_...)

1 change(s) wait for a person's sign-off. Run anlyon plan after they decide.
```

`--wait 30m` waits for the decisions and reports them. Exit codes: 0 when everything applied, 15 when a sign-off is pending, 9 when `--wait` ran out, 14 when a change failed or a sign-off was rejected or expired.

## Run plan on pull requests and apply on merge

Store an API key with the scopes above as the repository secret `ANLYON_API_KEY`, one per environment.

```yaml
# .github/workflows/anlyon.yml
name: Anlyon guardrails
on:
  pull_request:
    paths: [anlyon.yaml]
  push:
    branches: [main]
    paths: [anlyon.yaml]

permissions:
  contents: read
  pull-requests: write

jobs:
  guardrails:
    runs-on: ubuntu-latest
    env:
      ANLYON_API_KEY: ${{ secrets.ANLYON_API_KEY }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 22
      - name: Plan
        if: github.event_name == 'pull_request'
        run: npx --yes @anlyonhq/cli plan --markdown > plan.md
      - name: Comment the plan
        if: github.event_name == 'pull_request'
        env:
          GH_TOKEN: ${{ github.token }}
        run: gh pr comment ${{ github.event.pull_request.number }} --body-file plan.md --edit-last || gh pr comment ${{ github.event.pull_request.number }} --body-file plan.md
      - name: Apply
        if: github.event_name == 'push'
        run: npx --yes @anlyonhq/cli apply --yes
```

On merge, tightening changes apply and the job succeeds. Loosening ones exit 15 with their sign-off links in the job log, so the run shows as failed until a workspace admin decides. Pin the CLI version once you rely on it.

## Pull

```bash
anlyon pull --yes
```

Writes the live environment: every action except those of packs the file lists, the policies the file owns, the limits that are not archived, the budgets (when the credential can read them), and the secret names the actions reference. Line endings and comments elsewhere in the file are kept. `--yes` replaces an environment already in the file without asking.

## Related

- [Policy as code](/trust-control/policy-as-code): the policy file format and its tests.
- [Impact limits](/execution/impact-limits).
- [Set up from your agent](/setup-from-your-agent).
