---
sidebar_position: 9
title: Deploy-time configuration
---

# Deploy-time configuration

Four declarations that shape what happens **when somebody installs your agent**:
what they are asked, what is seeded for them, and what starts a run.

| Block | Writes to | Asked at |
|---|---|---|
| [`selections`](#selections) | the agent's Env bag — **configuration** | deploy |
| [`deployInput`](#deployinput) | the agent's default trigger payload | deploy |
| [`defaultSchedule`](#defaultschedule) | the agent's schedules | deploy (once) |
| [`workflow.json` → `triggers`](#triggers) | the agent's trigger settings | every deploy |

---

## `selections`

### What it is

A value the operator must **pick from a live list some integration owns** —
which board this agent works, which space it writes to — rather than dig an id
out of a URL and type it into an environment variable.

One declaration renders a dropdown of the real options the connected system
returns, and the answer lands in the agent's encrypted Env bag.

### Syntax

```js
spec: {
  selections: [{
    id: 'board',
    label: 'Board',
    from: 'board_tracker',                  // a provider, or a one-of group
    providerKey: 'BOARD_TRACKER',           // records WHICH provider was chosen
    keys: {
      vikunja: 'BOARD_VIKUNJA_PROJECT_ID',
      jira:    'BOARD_JIRA_PROJECT_KEY',
    },
    required: true,
    help: 'The board this agent polls for tickets marked ready.',
  }],
}
```

### Properties

| Name | Type | Required | Allowed values | Default | Update behavior |
|---|---|---|---|---|---|
| `id` | string | **Yes** | lowercase, starts with a letter, ≤ 41 chars, unique in the array | — | Seeded once |
| `label` | string | **Yes** | non-empty — it is what the picker is called | — | Card only |
| `from` | string | **Yes** | a provider id, or a one-of group the platform already knows | — | Seeded once |
| `keys` | object | **Yes** | `{ provider: 'ENV_KEY' }` — every provider must belong to `from` | — | Seeded once |
| `providerKey` | string | **Yes** when the group offers more than one provider | a writable Env key | none | Seeded once |
| `required` | boolean | No | | `true` | Seeded once |
| `help` | string | No | | none | Card only |

### Why `keys` is a map

The value written is a different *kind* of thing per provider: Vikunja addresses
a board by a numeric project id, Jira by a project key (`"PROJ"`). One
environment variable cannot express that, and collapsing them would leave the
reader guessing which system's id it holds.

### Gotchas

**A one-of group with more than one option needs a `providerKey`.** A choice
that records nothing deploys an agent that cannot tell which system it was
pointed at — so the declaration is refused.

**A provider with no key in `keys` is simply not offered.** That is legitimate:
some systems are reached another way and have no list to show.

**It is seeded once.** A value you have already set survives every Update, and
the deploy tells you what it kept.

---

## `deployInput`

### What it is

A field of your agent's input schema, annotated so the deploy asks for it once
and stores it as the agent's default trigger payload.

### Syntax

```js
// state.js
repoUrl: z.string().url()
  .meta({ deployInput: {
    required: true,
    source: 'github-repo',
    label: 'GitHub repo',
    help: 'Pick the repository whose flaky tests this agent should fix.',
  }}),

circleProjectSlug: z.string().optional()
  .meta({ deployInput: {
    source: 'circleci-project',
    deriveFrom: 'repoUrl',                 // resolved automatically, never typed
    label: 'CircleCI project',
  }}),
```

### Properties

| Name | Type | Required | Allowed values | Default | Update behavior |
|---|---|---|---|---|---|
| `required` | boolean | No | gates the deploy in the dashboard, the CLI **and** over MCP | `false` | Seeded once |
| `source` | string | No | which control renders it, or which resolver fills it | none | Seeded once |
| `deriveFrom` | string | No | another field's name — auto-resolved, never user-entered, never required of the user | none | Seeded once |
| `label` / `help` | string | No | | the field name | Card only |

### Gotchas

`deployInput.required` is the **deploy gate**, which is separate from whether
the field is optional in your schema.

Use `selections` instead when the value is agent *configuration* your code reads
from the environment — not part of a run's input.

---

## `defaultSchedule`

### What it is

A cron-driven agent does nothing until something wakes it. `defaultSchedule` is
the floor: deploy installs it as the agent's ordinary schedule **when the agent
has no schedule at all**.

### Syntax

```js
spec: {
  defaultSchedule: { cron: '*/15 * * * *', name: 'Board tick' },
}
```

### Properties

| Name | Type | Required | Allowed values | Default | Update behavior |
|---|---|---|---|---|---|
| `cron` | string | **Yes** | a **5-field** Unix cron (`m h dom mon dow`) | — | Seeded once |
| `timezone` | string | No | an IANA zone | `UTC` | Seeded once |
| `name` | string | No | | none | Seeded once |
| `input` | object | No | the trigger body | `{}` | Seeded once |

A one-shot date is refused — a template cannot know a date.

### Gotchas

**Any existing entry blocks the seed.** One you created, the seeded one after
you edited it, a paused one (pausing is a disable, not a delete), even a
one-shot that already fired. So an Update can never resurrect a schedule you
paused, and never re-points one you changed.

**One entry, not an array.** The declaration is a floor, and a floor is one
cadence. An agent that needs several schedules has an operator who set them, and
seed-once means those are never touched.

---

## `triggers`

### What it is

An optional `workflow.json` beside your graph, declaring how runs start. It is
re-applied on **every** deploy, including an Update.

### Syntax

```json
{ "triggers": { "events": ["github.pr.opened", "github.pr.updated"] } }
```

```json
{ "triggers": { "api": true } }
```

### Properties

| Name | Type | Required | Allowed values | Default | Update behavior |
|---|---|---|---|---|---|
| `api` | boolean | No | `true` ships the inbound webhook on by default | off (opt-in) | Re-applied |
| `events` | string[] | No | `github.pr.opened`, `github.pr.updated`, `github.pr.commented`, `github.pr.mentioned`, and the `gitlab.mr.*` equivalents | `[]` | Re-applied |

Inbound webhooks are routed **by subscription, not by agent name**: a delivery is
normalised to an event, the repository is resolved to a project, and every
subscribed agent gets its own independent run.

### Gotchas

**A member of a fleet is suppressed from event routing while it is a member.**
Otherwise one pull request fires both the fleet — which runs the member itself —
and the standalone member, giving you two reviews. Deploy the member on its own
and it starts responding again.

---

## Sample trigger bodies

Two card-only fields that make the Trigger dialog usable:

```js
spec: {
  triggerExample:  { ticketKey: 'PROJ-123' },
  triggerExamples: [
    { label: 'board tick', body: '{}' },
    { label: 'one ticket', body: '{"ticketKey":"PROJ-123"}' },
    { label: 'plan from a PRD', body: '{"prd":"# Feature\\n\\nAs a user I want …"}' },
  ],
}
```

`triggerExample` pre-fills the editable body. `triggerExamples` renders one
click-to-use reference per source below it. Both must be **valid JSON** — JSON
has no comments.

## See also

- [Update behavior](./update-behavior.md)
- [Composition](./composition.md)
