# Goal manifest

Every key in kortix.yaml, the goal manifest: agents, connectors, triggers, sandbox and env.

Canonical page: https://app.auto-lab.ai/docs/developers/manifest

`kortix.yaml` at the root of the [goal's repository](/docs/developers/repository) is the goal manifest; the UI calls
it the project manifest. It says which agents may run and what each may touch, which connectors and triggers the
goal has, which machine sessions start on and which secret names the goal expects. This page is the reference for
version 2, which every new goal uses.

## Check the file

The schema is public. The first line of the example below points your editor at it. `kortix validate` checks the
file in a clone, and `kortix schema --version 2` prints the schema. The same check runs when a change request
merges, and an invalid manifest blocks the merge. Top-level keys Auto Lab does not know are skipped rather than
rejected, so the file can carry your own notes. A version higher than 2 is refused. Older goals may still have a version 1 `kortix.toml`; new goals
never do.

## A full example

```yaml
# yaml-language-server: $schema=https://app.auto-lab.ai/schema/kortix.v2.schema.json
kortix_version: 2
default_agent: kortix

env:
  required: [CRM_API_KEY]
  optional: [FORM_WEBHOOK_SECRET]

sandbox:
  default: data
  templates:
    - slug: data
      name: Data tools
      dockerfile: .kortix/Dockerfile
      cpu: 4
      memory: 8

agents:
  kortix:
    connectors: all
    secrets: all
    skills: all
    kortix_cli: all
  reporter:
    connectors: [hubspot, gmail]
    connectors_required: [hubspot]
    secrets: [CRM_API_KEY]
    skills: [weekly-brief]
    kortix_cli: [project.read, project.gitops.push]

connectors:
  - slug: hubspot
    provider: composio
    app: hubspot
    policies:
      - match: "*delete*"
        action: block
  - slug: gmail
    provider: composio
    app: gmail
    authorization_strategy: user
    sensitive: true

policies:
  - match: "gmail.*send*"
    action: require_approval
policy:
  default_mode: risk

triggers:
  - slug: monday-brief
    name: Monday brief
    type: cron
    agent: reporter
    cron: "0 0 8 * * 1"
    timezone: Europe/London
    prompt: Draft this week's competitor brief and send it to me for review.
  - slug: new-lead
    type: webhook
    secret_env: FORM_WEBHOOK_SECRET
    filter:
      body.type: lead
    prompt: |
      New lead from {{ body.email }} at {{ body.company }}.
      Check it against our qualification rules.
```

The example assumes the repository has `.kortix/opencode/agents/reporter.md`, a `weekly-brief` skill and both
secrets.

## Top-level keys

| Key | Required | Default | What it sets |
|---|---|---|---|
| `kortix_version` | yes | | Must be `2`. |
| `default_agent` | yes | | The agent a session or trigger runs when it names none. Must be a declared agent that is not disabled. |
| `agents` | yes | | What each agent may touch. At least one agent. |
| `project` | no | | `name` and `description`, for people reading the file. |
| `env` | no | | The secret names the goal expects. |
| `sandbox` | no | Auto Lab default | Machine images and which one sessions use. |
| `connectors` | no | | The tools agents can call. |
| `policies` | no | | Tool rules that apply across every connector. |
| `policy.default_mode` | no | `allow_all` | What happens to a tool call no rule matches. |
| `triggers` | no | | Scheduled and webhook automation. |
| `opencode.config_dir` | no | `.kortix/opencode` | Where sessions read agents and skills. Leave it: the Chat and learning always use `.kortix/opencode`. |
| `runtime` | no | `opencode` | Leave it unset. `pi` is an experiment behind a feature flag. |
| `apps` | no | | Only read when the Apps feature is switched on for the goal. It is off by default. |

A `channels` or `sandboxes` key is an error. Channels are connected in **Settings › Channels**, not in the file.

## `agents`

`agents` maps an agent name to what it is allowed to touch. It holds no behaviour. The prompt, model, mode,
temperature and tool permissions live in the agent's own file, `.kortix/opencode/agents/<name>.md`, and the map key
must match that file name. Putting one of those fields in `kortix.yaml` is an error. See
[Agents and skills](/docs/brain/agents-and-skills).

| Field | Default | Notes |
|---|---|---|
| _(name)_ | | Lowercase letters, digits, `-` and `_`, up to 128 characters. |
| `enabled` | `true` | `false` keeps the agent from starting. |
| `connectors` | none | Connector slugs it may call, or `all`. |
| `connectors_required` | none | Connectors that must have a working connection before a session starts. Each must also be in `connectors`. |
| `secrets` | none | Secrets it receives, by name, or `all`. |
| `skills` | none | Skills it may load, by name, or `all`. |
| `kortix_cli` | none | What it may do with the `kortix` CLI and the API, as a list of permissions, or `all`. |
| `sandbox` | | The slug of the sandbox template its sessions start on. |
| `workspace` | `branch` | `branch` gives its sessions the repository on their own branch. `runtime` and `read` start them without a copy of the repository. |

Grants are deny-by-default. A field you leave out means nothing is granted. `all` never lifts an agent above the
person who started it. `kortix_cli` takes goal-level permissions only, such as `project.read`,
`project.gitops.push` or `project.trigger.read`; run `kortix validate --scopes` for the full list. Organization
permissions can never be granted to an agent. Even with `project.gitops.merge`, a session cannot merge a change
request it opened.

The Chat's access to connectors follows the `connectors` grant of an agent named `harness-main` when the manifest
declares one, and the grant of `default_agent` otherwise.

## `connectors`

A connector is a tool agents can call. The definition lives in the file. The credential lives in Auto Lab and never
in git. See [Connectors](/docs/connect/connectors).

| Field | Default | Notes |
|---|---|---|
| `slug` | required | Lowercase, unique among connectors. `kortix_slack`, `kortix_teams` and `kortix_email` are reserved for channels. |
| `provider` | required | See the next table. |
| `name` | the slug | Display name. |
| `enabled` | `true` | |
| `authorization_strategy` | `project` | `project` uses the goal's shared connection. `user` uses only the connection of the person the agent acts for. |
| `sensitive` | `false` | `true` makes every call ask first, reads included, unless a rule allows it. |
| `policies` | | Rules for this connector's tools. `match` is a pattern over tool names, `action` is `always_run`, `require_approval` or `block`. |
| `auth` | | How to send the credential: `type` (`bearer`, `basic`, `api_key`, `custom`, `oauth1`, `hmac`, `aws_sigv4`, `mtls` or `none`), `in` (`header`, `query` or `cookie`) and `name` for `api_key` and `custom`. |
| `headers` | | Fixed request headers, up to 32. They are plain text in git, so never put a credential here. |

| Provider | Needs | Notes |
|---|---|---|
| `composio` | `app` | An app from the connector catalogue, such as `gmail`. |
| `pipedream` | `app` | |
| `mcp` | `url` | `transport` is `http` (default) or `sse`. |
| `openapi` | `spec` | A URL or a path in the repository. |
| `postman` | `spec` | A collection URL or path. |
| `graphql` | `endpoint` | `spec` is optional. |
| `http` | `base_url` | `spec` is optional. |
| `channel` | `platform` | `slack`, `teams` or `email`. Auto Lab writes these when you connect a channel. |

## Tool rules

Patterns use `*` as a wildcard and ignore case. A connector's own `policies` match its tool names. The top-level
`policies` match `<connector>.<tool>` and apply to every connector. Auto Lab decides each call in this order, and
the first answer wins:

1. Top-level `policies`, top to bottom.
2. The connector's `policies`.
3. `sensitive: true` on the connector: ask first.
4. `policy.default_mode`: `allow_all` runs the call. `risk` runs reads and asks before writes and deletes.

The **Delegation** dial writes these keys for you. **Asks before acting** adds a first rule that asks before every
call and sets `risk`. **Acts on its own** removes that rule and sets `allow_all`. **Acts within your rules** puts
your own rules and mode back. See [Approvals and autonomy](/docs/goals/approvals).

## `triggers`

A trigger runs the agent on a schedule or when a signed HTTP request arrives. See
[Plans, schedules and triggers](/docs/goals/schedules).

| Field | Default | Notes |
|---|---|---|
| `slug` | required | Lowercase, unique among triggers. |
| `type` | required | `cron`, `webhook` or `monitor`. |
| `prompt` | required | The message the agent receives. `{{ … }}` placeholders are filled from the payload. |
| `name` | the slug | Display name. |
| `agent` | `default_agent` | Must name a declared agent. Leave it out rather than writing `default`. |
| `enabled` | `true` | `false` keeps the trigger without firing it. |
| `model` | chosen when it fires | Pins a model, as `provider/model`. See [Models](/docs/brain/models). |
| `session_mode` | `fresh` | Which session a fire uses. See below. |
| `filter` | | Payload paths and the values they must equal, such as `body.type: lead`. A delivery that does not match is accepted and ignored. |

**Cron.** Set `cron` to a six-field expression (second, minute, hour, day of month, month, day of week), or
`run_at` to one ISO-8601 time for a single run. `timezone` is an IANA name and defaults to `UTC`.

**Webhook.** The URL is `https://api.auto-lab.ai/v1/webhooks/projects/<goal-id>/<slug>`. `secret_env` names the
secret that holds the signing key. Sign the raw body with HMAC-SHA256 and send `X-Kortix-Signature: sha256=<hex>`;
GitHub's `X-Hub-Signature-256` also works. A sender that cannot sign can send the key itself in `X-Kortix-Token` or
`Authorization`. The prompt can use `{{ body.<path> }}`, `{{ headers.user_agent }}`, `{{ trigger.slug }}` and
`{{ fired_at }}`. The secret must be delivered to connectors only, never to a machine:

```bash
kortix secrets delivery FORM_WEBHOOK_SECRET none --consumer connector
```

**Monitor.** An always-on command from the repository whose output lines are events. Monitors are experimental
and need the **Monitors** flag in [Feature flags](/docs/goals/settings#feature-flags), which is off by default.

**Session mode.** `fresh` starts a new session on every fire. `reuse` sends each fire to the session this trigger
started last, so context builds up; monitors use it by default. `pinned` uses the session in `session_id`. A
`session_key` such as `{{ body.chat_id }}` keeps one session per value; an empty key falls back to `fresh`.

On a goal with the Chat, a webhook or monitor fire does not start a session. It wakes the Chat with the trigger's
prompt and a summary of the payload. If the agent was not waiting for that event, the turn can only read, not act.
Cron triggers still start a session, and those sessions count against **Settings › Autonomy** limits (**New
sessions per day**, **Sandbox minutes per day**). The plan's routine and check-ups are not manifest triggers on
these goals. They are the agent's own schedules in **Brain › Schedules**.

## `env`

`env.required` and `env.optional` list names only. Values are [secrets](/docs/connect/secrets) and never go in the
file. **Settings › Secrets** lists the declared names so people know what to fill in; a missing `required` value
does not stop a session. Names use letters, digits and `_`, do not start with a digit, are stored in upper case and
fit in 64 characters. `KORTIX_` names are reserved. An agent receives a secret only through its `secrets` grant.

## `sandbox`

`sandbox.templates` is a list of machine images. Each entry takes these fields:

| Field | Notes |
|---|---|
| `slug` | Required and unique. Up to 64 characters in practice. `default` is reserved. |
| `name` | Display name. Defaults to the slug. |
| `dockerfile` | A path inside the repository. Set this or `image`, not both. |
| `image` | A public image with a pinned tag or digest, such as `python:3.12-slim`. `latest` gets a warning. |
| `entrypoint` | Leave it unset. |
| `cpu`, `memory`, `disk` | 1 to 32 vCPUs, 1 to 128 GiB of memory, 1 to 500 GiB of disk. |

A value below the minimum is an error. A value above the maximum gets a warning and is capped. GPUs are not
supported. `sandbox.default` must name a template declared here, or `default` for the Auto Lab image. An agent's
own `sandbox` key wins over it. For Dockerfile rules and rebuilds, see
[Sandbox templates](/docs/developers/repository#sandbox-templates).

## When changes take effect

- **A change in a session or a clone** takes effect once its change request merges into the default branch. New
  sessions start from that branch, and the Chat picks the change up within about a minute.
- **A change saved in the UI** (a trigger, a connector, a tool rule, the **Delegation** dial) is committed to the
  default branch at once. Auto Lab rewrites the file from its parsed contents when it saves, so comments are
  dropped and `kortix_version` moves to the top.
- **A webhook trigger** answers `404` until its entry is on the default branch and enabled.
- **A sandbox change** builds a new image. Running sessions keep the machine they started on.
- **A secret value** is read when a machine starts, so it applies to the next session.
