> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cotool.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Response Agents as Code

> Manage response agents as YAML in your own GitHub repo, synced automatically into Cotool

Response Agents as Code lets you define and manage your response agents as **YAML files in your own GitHub repository**. Cotool polls the repo on a schedule, reconciles the files into your agents, and records every synced change in the agent's version history.

This gives you a GitOps workflow for agents: code review on pull requests, change history in Git, and the ability to roll out the same agents across environments by editing files instead of clicking through the UI.

<Note>
  **Managed vs. manual agents**: An agent synced from a repo is **engine-owned** — it is read-only in the Cotool UI. You change a managed agent by editing its YAML and letting the next sync apply it. Agents created in the UI are unaffected; the two can coexist in the same organization.
</Note>

## How It Works

<Steps>
  <Step title="You keep agent YAML in a Git repo">
    Each agent is one YAML file in a folder you choose (default `cotool/agents`). The file describes the whole agent: prompt, model, tools, skills, inputs, triggers, and more.
  </Step>

  <Step title="Cotool polls and reconciles">
    Roughly every 5 minutes, Cotool fetches the configured branch and makes your agents match the files — creating new agents, updating changed ones, and removing agents whose file disappeared. A commit-SHA check skips the fetch entirely when the branch hasn't moved.
  </Step>

  <Step title="Every change is versioned">
    Each synced create or update appends an entry to the agent's [version history](/improving-agents/system-prompt-versioning), tagged with the source commit. An unchanged re-sync adds nothing.
  </Step>
</Steps>

## Setup

Response Agents as Code syncs from the [GitHub integration](/integrations/dev-tools/github). Connect GitHub first, then configure the sync.

<Steps>
  <Step title="Connect GitHub">
    Go to **Settings > Integrations** and connect GitHub. Install the GitHub App on the organization and repositories you want to sync from.
  </Step>

  <Step title="Open the Response Agents as Code card">
    On the GitHub tool page, find the **Response Agents as Code** card. (You need the `tool.manage` permission to configure it.)
  </Step>

  <Step title="Choose repo, branch, and path">
    Pick the repository, branch, and the folder that contains your agent YAML files. The dropdowns are populated from the repos the GitHub App can access. The path defaults to `cotool/agents`.
  </Step>

  <Step title="Enable and save">
    Turn on **Enable sync** and click **Save config**. While enabled, the periodic sync includes your organization; while off, it skips it.
  </Step>

  <Step title="Sync now (optional)">
    Click **Sync now** to run a sync immediately instead of waiting for the next cycle. The card shows last-sync status and a per-file table with the synced agent or any error.
  </Step>
</Steps>

## File Format

Each file defines one response agent. The format is intentionally distinct from the UI's bulk export/import format — here it's **one agent per file**, with the system prompt either inline or in a separate `.md` file.

```yaml theme={null}
apiVersion: cotool.ai/v1
kind: ResponseAgent
metadata:
  sync_key: triage-suspicious-login
  name: Triage Suspicious Login
  description: First-pass triage for IdP suspicious-login alerts.
  tags:
    - auth
    - p1
spec:
  modelAlias: anthropic:chat:sonnet-4.6
  planningMode: auto
  toolNames:
    - slack
  inputs:
    - name: alertId
      type: text
      description: The IdP alert ID to triage.
      required: true
  acceptanceCriteria:
    - Determines whether the login is expected travel or genuinely anomalous.
  triggers:
    - source: cron
      name: Hourly sweep
      schedule: "0 * * * *"
  systemPrompt:
    inline: |
      # Triage Suspicious Login

      You are a first-pass triage agent for identity-provider
      "suspicious login" alerts. Classify each as Expected or Anomalous.
```

### Top-level fields

| Field        | Required | Notes                                         |
| ------------ | -------- | --------------------------------------------- |
| `apiVersion` | optional | Schema version, e.g. `cotool.ai/v1`.          |
| `kind`       | optional | Must be `ResponseAgent` when present.         |
| `metadata`   | required | Identity and human-readable info (see below). |
| `spec`       | required | The agent definition (see below).             |

### `metadata`

| Field         | Required | Notes                                                                  |
| ------------- | -------- | ---------------------------------------------------------------------- |
| `sync_key`    | optional | Stable, org-unique identity for the agent (see [Identity](#identity)). |
| `name`        | required | Human-readable name. Can change freely — it is not the identity.       |
| `description` | optional | What the agent does.                                                   |
| `tags`        | optional | Free-text tags for categorizing the agent.                             |

### `spec`

| Field                                                                | Required | Notes                                                                                                       |
| -------------------------------------------------------------------- | -------- | ----------------------------------------------------------------------------------------------------------- |
| `modelAlias`                                                         | required | Model alias, e.g. `anthropic:chat:sonnet-4.6`.                                                              |
| `systemPrompt`                                                       | required | Either `{ inline: "..." }` or `{ file: "prompt.md" }` (path relative to the YAML file).                     |
| `planningMode`                                                       | optional | `auto` (default), `never`, or `always`. See [Planning Modes](/agents/planning-modes).                       |
| `agentFilesystem`                                                    | optional | Mount a persistent [Agent Filesystem](/agents/agent-filesystem) across runs (default `false`).              |
| `toolNames`                                                          | optional | Non-agent tool ids the agent may use.                                                                       |
| `agentTools`                                                         | optional | Other managed agents this agent may call, by `sync_key` (see [Agent-to-agent](#agent-to-agent-references)). |
| `cliNames`                                                           | optional | CLI integration ids the agent may use.                                                                      |
| `skills`                                                             | optional | Attached [skills](/agents/skills) by name (org-scoped, tracks the latest version).                          |
| `inputs`                                                             | optional | Declared agent inputs.                                                                                      |
| `acceptanceCriteria`                                                 | optional | [Acceptance criteria](/improving-agents/acceptance-criteria) evaluated for each run.                        |
| `triggers`                                                           | optional | Inline triggers (see [Triggers](#triggers)).                                                                |
| <span style={{whiteSpace: 'nowrap'}}>`structuredOutputSchema`</span> | optional | JSON schema for [structured output](/agents/structured-outputs).                                            |
| `contextDocs`                                                        | optional | [Context documents](/agents/context-documents) attached to the agent.                                       |

<Note>
  The YAML is **strict** — unknown or misspelled keys (for example `syncKey` instead of `sync_key`) are rejected with a validation error for that file rather than being silently ignored.
</Note>

## Identity

Each file maps to exactly one agent. Cotool decides which agent a file represents by:

1. **`metadata.sync_key`** — an author-chosen identity, unique within your organization. **Recommended.** Because identity is independent of file content and location, you can rename the agent or move/rename the file without churning the agent or losing its triggers and history.
2. **File path** — used when no `sync_key` is set. Renaming or moving the file then changes its identity (Cotool treats it as a new agent).

<Note>
  Set a `sync_key` for any agent you expect to rename or reorganize. It's the only way to keep a webhook trigger's minted secret and URL stable across a rename.
</Note>

## Triggers

Declare triggers inline under `spec.triggers`. The sync engine materializes them as engine-owned, read-only triggers on the agent. Supported sources: `cron`, `webhook`, `jira`, `jira-automation`, `slack`, `linear`, and `email`.

```yaml theme={null}
spec:
  triggers:
    - source: cron
      name: Daily report
      schedule: "0 9 * * *"
    - source: webhook
      name: Inbound events
      sync_key: inbound          # stable id; preserves the minted secret across renames
    - source: jira
      name: New security issues
      events:
        - jira:issue_created
      jqlFilter: project = SEC
    - source: slack
      name: Security channel
      allowedChannelIds:
        - C0123456789
```

<Warning>
  **Secrets never live in YAML.** Webhook secrets and inbound URLs are minted server-side; Slack and Linear bind to your organization's connected integration at sync time. If the required integration isn't connected, that trigger is **skipped with a diagnostic** — the agent itself still syncs, and the file's status shows *integration not connected* rather than failing.
</Warning>

A trigger's `sync_key` is its stable identity **within the agent** (it defaults to a slug of `name`). Set it explicitly to rename a trigger without recreating its row — which, for webhook-family triggers, is what preserves the minted secret and URL.

For the full set of fields per source, see [Triggering Agents](/agents/triggering-agents).

## Agent-to-Agent References

A YAML agent can call another managed agent as a tool by referencing its `sync_key`:

```yaml theme={null}
spec:
  toolNames:
    - slack
  agentTools:
    - sync_key: enrich-alert
    - sync_key: notify-oncall
```

References are resolved in a second pass after all agents exist, so **forward references work** — the order of files doesn't matter. Always reference other agents via `agentTools` and `sync_key`, not via `toolNames`.

## Reconcile Behavior

Each sync makes your agents match the files:

* **New file** → a new managed agent is created.
* **Changed file** → the managed agent is updated, and the change is recorded as a version.
* **Removed file** → the agent is **gently soft-deleted**. This keeps its triggers and tags and is reversible: if the file returns, the *same* agent is resurrected (not a fresh copy).
* **Invalid file** → the last good version of that agent is **kept**. The error is recorded against that file and never blocks the others, so one bad file can't take down the rest of your agents.

The card's per-file status table shows `ok`, `integration not connected`, or a parse/validation error for each file, with a link to the file at the synced commit.

## Validate Before Syncing

`POST /api/agent-sync/validate` runs the **exact same** parse, schema, and cross-file graph validation as the sync engine, statelessly, over files you post — so it cannot drift from real sync behavior. It's the engine behind the CI Action below, and you can call it directly:

```bash theme={null}
curl -X POST https://app.cotool.ai/api/agent-sync/validate \
  -H "Authorization: Bearer $COTOOL_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "files": [
      { "path": "cotool/agents/triage.yaml", "content": "apiVersion: cotool.ai/v1\nkind: ResponseAgent\n..." }
    ]
  }'
```

The response carries a result per YAML file with `errors` (would block sync) and `warnings`, plus an overall `valid` flag. Warnings are checks that depend on live org state the request can't see (an unprovided `systemPrompt.file`, or an `agentTools` sync\_key not in the posted set) and never fail the check. See the [API Reference](/api-reference/agentsync/validate-response-agent-as-code-yaml) for the full schema.

<Warning>
  **Prefer GitHub OIDC over an API key.** The `$COTOOL_API_KEY` above is shown for direct or manual calls and for CI outside GitHub. For GitHub Actions, use the [OIDC-based Action](#validate-in-ci) below instead — it mints a short-lived, repo-scoped token, stores no secret, and authorizes as a **validate-only** principal.

  A Cotool API key, by contrast, is **long-lived** and carries the **full permissions of the user who created it** — it is *not* narrowed to validation, even though the endpoint only needs `tool.manage` — so a leaked key is as powerful as that user. If you must use one (non-GitHub or self-hosted CI), create it under a dedicated, least-privilege service account, keep it in a secrets manager, and rotate it regularly.
</Warning>

## Validate in CI

The [`cotool/validate-agents`](https://github.com/cotool/validate-agents) GitHub Action validates your agent YAML on every pull request and annotates problems inline.

The Action authenticates **only** via GitHub OIDC — there is no API-key input, and it is the **recommended** way to validate in CI. The job mints a short-lived, repo-scoped OIDC token; Cotool verifies it and maps the repository to your org via your GitOps sync configuration. Nothing to store or rotate, and the token grants validate-only access — never your broader API-key permissions.

```yaml theme={null}
# .github/workflows/validate-agents.yml
on:
  pull_request:
    paths: ['cotool/agents/**']
permissions:
  contents: read
  id-token: write          # lets the job mint the OIDC token
jobs:
  validate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v5
      - uses: cotool/validate-agents@v1
        with:
          dir: cotool/agents
```

<Note>
  OIDC requires that this repository is configured as your org's agents source under **Settings → Agents → GitOps sync** — that registration is what authorizes the repo. To validate before configuring sync, or outside GitHub CI, call the endpoint directly with an API key (the `curl` example above) rather than the Action — subject to the API-key trade-offs noted above.
</Note>

### Inputs

| Input              | Default                 | Description                                                         |
| ------------------ | ----------------------- | ------------------------------------------------------------------- |
| `dir`              | `cotool/agents`         | Directory to scan for agent YAML (and referenced prompt `.md`).     |
| `file`             | —                       | Comma-separated explicit file list; overrides directory discovery.  |
| `recursive`        | `true`                  | Recurse into subdirectories of `dir`.                               |
| `audience`         | `cotool-validate`       | OIDC audience requested from GitHub and verified by the Cotool API. |
| `api_url`          | `https://app.cotool.ai` | Base URL of the Cotool API.                                         |
| `fail_on_warnings` | `false`                 | Fail the check on warnings (errors always fail).                    |

### How validation gates a merge — and how syncing stays safe

The Action (and the `/validate` endpoint behind it) is **advisory by default**: it reports problems on the pull request, but on its own it does not stop anything from syncing. Cotool's sync engine polls whatever is on your **configured branch** and reconciles it — it does not know or care whether a CI check ran or passed. Two separate mechanisms turn that into a safe GitOps workflow:

1. **Branch protection makes validation a merge gate (recommended).** In your repository settings, mark the validation check as a **required status check** on the branch you sync from. GitHub then blocks any pull request from merging until validation passes, so only validated YAML ever reaches the branch Cotool polls. This is the piece that actually *gates* syncing, and it lives in **your** GitHub settings — not in Cotool.
2. **The sync engine re-validates as a backstop.** Independently of CI, every sync runs the exact same parse, schema, and cross-file graph checks before applying a file. An invalid file is **skipped per-file** — its last good version is kept and the error is recorded against that file — so a bad file that slips past CI never corrupts your live agents (see [Reconcile Behavior](#reconcile-behavior)).

<Steps>
  <Step title="Open a pull request">
    An author edits agent YAML under your synced path and opens a PR. The `validate-agents` Action runs against the PR's files and annotates any errors or warnings inline.
  </Step>

  <Step title="The required check gates the merge">
    With branch protection enabled on your sync branch, the PR cannot merge until the validation check is green. Reviewers only ever merge validated YAML; broken YAML is blocked at the door.
  </Step>

  <Step title="Merge to the sync branch">
    Once approved and green, the PR merges into the branch you configured under **Settings → Agents → GitOps sync** (commonly your default branch).
  </Step>

  <Step title="Cotool polls and reconciles">
    Within \~5 minutes the sync engine fetches the branch HEAD, re-validates, and applies the changes — recording each as a version. Anything still invalid is skipped per-file, never blocking the rest.
  </Step>
</Steps>

<Note>
  Cotool never reads your CI status — **branch protection** is what ensures only validated YAML lands on the sync branch. If you sync from an unprotected branch (or commit directly to it), the Action becomes a non-blocking signal and the **server-side backstop** is your only guard against invalid files.
</Note>

## Exporting an Existing Agent

To move a UI-built agent into Git, open the agent, use the version dropdown, and choose **Export as YAML**. The export emits the canonical sync shape — inline prompt as a literal block, stable `sync_key`s for agent-tool references, and triggers without any server-owned secrets — ready to commit to your repo.

## Recovery

If the backing GitHub integration is disconnected, **synced agents keep running** — only syncing pauses. The card surfaces a *Reconnect GitHub* prompt.

If you need to edit a managed agent while GitHub is disconnected, use **convert to manual**: Cotool makes an editable copy, moves the triggers to it, and parks the managed original (recoverable). The copy is an ordinary UI-owned agent from then on.

## Related

<CardGroup cols={2}>
  <Card href="/improving-agents/system-prompt-versioning" icon="code-branch">
    **Agent Versioning** — how synced and manual changes share one timeline
  </Card>

  <Card href="/agents/triggering-agents" icon="bolt">
    **Triggering Agents** — all trigger types and their fields
  </Card>

  <Card href="/integrations/dev-tools/github" icon="github">
    **GitHub Integration** — connect GitHub and configure repository sync
  </Card>

  <Card href="/agents/creating-agents" icon="wand-magic-sparkles">
    **Creating Agents** — building agents in the UI
  </Card>

  <Card href="/agents/skills" icon="copy">
    **Skills** — reusable instructions referenced by name
  </Card>
</CardGroup>
