# Run Metadata

Every agent run carries **metadata**: a small set of named values that describe the run, alongside its input. The input is what the agent works on, such as a question, an article or an email body. Metadata is the context around it, such as who asked, which customer, which channel, the email's sender or the article's URL.

Steps read metadata with `{{metadata.<key>}}`, in any field that takes [string substitutions](https://seclai.com/docs/agent-steps#string-substitutions), and in conditions, filters and Call Agent mappings:

```
You are helping {{metadata.customer_name}} on the {{metadata.plan}} plan.
Reply in {{metadata.language}}.
```

## Every key is declared

A trigger **declares** the metadata keys its agent's steps may use. The declaration is the agent's metadata interface: a caller can read it to learn what to send, and the platform checks runs and saves against it.

- A step may use only a key that one of the agent's triggers declares. Saving an agent whose steps use an undeclared key is refused, and the error names the key and where it is used. An import, and some API and MCP saves, [declare such keys for you](#api-and-mcp) instead.
- Each declared key is **optional** or **required**.
- A declared key that a run doesn't carry reads as empty in a template, and as null in a [condition](https://seclai.com/docs/agent-steps/control).

A declaration entry has three fields:

```json
{ "key": "customer_id", "origin": "custom", "required": true }
```

| Field      | Meaning                                                                             |
| ---------- | ----------------------------------------------------------------------------------- |
| `key`      | The name steps use, as in `{{metadata.customer_id}}`                                |
| `origin`   | `system`, `source` or `custom` — see [Where keys come from](#origins)               |
| `required` | Only a custom key can be required; a run started without it is [refused](#refusals) |

A key can be up to 255 characters, and can't be `indexing_state` or start with `_seclai.`, which the platform uses internally. A key containing `{` or `}` can't be referenced, so the editor refuses one.

## Where keys come from

| Origin     | Who sets it                                                                                                     | Can be required |
| ---------- | --------------------------------------------------------------------------------------------------------------- | --------------- |
| **System** | The platform, on every run: `trigger_type`                                                                      | No              |
| **Source** | The platform, from what started the run: the email, the Slack message, the cloud-drive file or the content item | No              |
| **Custom** | A caller who starts the run, a trigger's default value, or a Slack form field                                   | Yes             |

System and source keys reach the run, and show in its [trace](https://seclai.com/docs/agent-traces), whether or not they are declared.

### System keys

| Key            | Value                                                                  |
| -------------- | ---------------------------------------------------------------------- |
| `trigger_type` | The type of the trigger that started the run, such as `email_received` |

### Source keys

Each trigger type that starts from an event has its own set:

- **Email Received** — the sender, recipients, subject, threading headers and authentication results. See [Captured Metadata](https://seclai.com/docs/agent-triggers#email-received-metadata).
- **Slack command** — the workspace, channel, thread, the person and the text they sent. See [Captured Metadata](https://seclai.com/docs/agent-triggers#slack-command-metadata).
- **Cloud file triggers** — the file's name, path, size, type and the drive event. See [Cloud File Triggers](https://seclai.com/docs/agent-triggers#cloud-file-triggers).
- **Content triggers** — the platform keys below, plus any field of the content item's own metadata.

| Content key                            | Value                                                                                                     |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------- |
| `content_guid`                         | The item's identifier within its source, such as a feed entry's GUID. Set on every content run.           |
| `url`                                  | A link to the item in Seclai                                                                              |
| `source_connection_id`                 | The id of the content source the item came from                                                           |
| `source_connection_content_version_id` | The id [`GET /contents/{id}`](https://seclai.com/docs/contents) takes. It can link to a newer version than the run's input. |

A content trigger can also declare any field of the item's metadata by name, such as `author` or `published_date`. Which fields an item has depends on its source.

### Custom keys and default values

A custom key gets its value from one of three places:

1. **The caller** — whoever starts the run sends it. Apart from a Slack form's fields, only a **Dynamic Input** or **Template Input** trigger takes custom keys from a caller, because those are the only types the API and MCP start directly.
2. **A default value** set on the trigger. When a caller also sends the key, **the caller's value wins**; the default is used only when the caller sends nothing.
3. **A Slack form field** — each field's key is a custom key, required when the field is.

On every other trigger type (email, Slack, content, cloud file), nothing sends custom keys, so a custom key there needs a default value or a Slack form field.

A key with a default value is always present, so it can't also be required.

## When a run or a save is refused

**A run started by a caller** — the Run button, the API, MCP, a parent agent's [Call Agent](#call-agent) step or a Slack user — is refused before any step runs when it:

- lacks a required custom key, or
- sends a custom key the trigger doesn't declare.

The refusal names every such key, and the run uses no credits:

```
Run refused: missing required metadata: customer_id; metadata the trigger does not declare: cutomer_name
```

The API returns it as an HTTP 400, MCP as an `error`, a Call Agent step fails with it, and a Slack user sees it in the thread.

**A run the platform starts** — an email, a content event, a cloud file, a schedule — is never refused for its metadata. A declared key it doesn't carry reads as empty.

**A save is refused** when, after the change:

- a step uses a key no trigger declares,
- a scheduled Template Input trigger has a required key, or
- a Call Agent step's mapping names a key that isn't a custom key the called agent declares, or leaves out one it requires.

```
Undeclared metadata: 'region' (referenced at summarize.prompt_template) is not declared on any trigger
```

## Declaring keys in the editor

### The trigger's Metadata step

Open the trigger and go to its last step, **Metadata**:

- **System metadata** lists every platform key for the trigger's type, with what it holds, an example value, and whether it is set on every run or only when present. Tick a key to declare it. A content trigger also lists the fields its knowledge base's items carry, with **On items**: how many items have a value for each, out of all items or those that arrived in the last 30 or 7 days. Hover a count to see the field's most frequent values. A field with a low count is empty on most runs. **Add content field** declares a field no item carries yet.
- **Custom metadata** lists the trigger's custom keys. Each has a **Default value**, and on a Dynamic or Template Input trigger a **Required** box; setting a default turns Required off. A Slack trigger's form fields are listed here too, read-only.
- A key the agent's steps use is marked **Used by steps**. If a step uses a key the trigger doesn't declare, a warning lists it before you save.

### From a step

In a step's field, the **Insert Metadata Field** picker lists the keys the agent's triggers declare, grouped as System, Source and Custom. A custom key shows **Required**, **Optional**, or its default value (`= gold`). Click one to insert it.

When the agent has one trigger, the picker can also declare as it inserts:

- **Platform metadata you can add** lists the trigger type's platform keys not yet declared; one click declares and inserts.
- **Add a metadata field** declares a new key, with an optional **Default value** and **Required**. On a trigger no caller sends custom keys to, the default value is required.

A `{{metadata.<key>}}` you type yourself for an undeclared key is flagged under the field as **not declared** and, when the agent has one trigger, offers **Declare on trigger** to fix it.

## Declaring keys from the API and MCP

Read a trigger's declaration, and the platform keys it could declare:

```bash
curl https://api.seclai.com/agents/{agent_id}/triggers/{trigger_id}/metadata-keys \
  -H "X-API-Key: YOUR_API_KEY"
```

Replace it — the body is the whole declaration:

```bash
curl -X PUT https://api.seclai.com/agents/{agent_id}/triggers/{trigger_id}/metadata-keys \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "metadata_keys": [
      { "key": "trigger_type", "origin": "system", "required": false },
      { "key": "customer_id", "origin": "custom", "required": true },
      { "key": "channel", "origin": "custom", "required": false }
    ]
  }'
```

For a content trigger, see how many of its knowledge base's items carry each field, over all time and the last 30 and 7 days, with each field's most frequent values:

```bash
curl https://api.seclai.com/knowledge_bases/{knowledge_base_id}/metadata-key-stats \
  -H "X-API-Key: YOUR_API_KEY"
```

Default values are set on the trigger's Metadata step in the editor, and an exported agent carries them. A definition saved through the API with `Seclai-Version: 2026-11-02` or later is refused if it uses an undeclared key; with an earlier version, each such key is declared optional for you. Importing an agent declares what its steps use the same way. Neither can declare a custom key on a trigger no caller sends custom keys to; that key needs a default value.

In MCP, `get_trigger_metadata_keys` and `set_trigger_metadata_keys` read and replace a declaration, and `get_knowledge_base_metadata_key_stats` counts the fields a content trigger's items carry. `update_agent_definition` takes `declare_metadata: true` to declare, as optional, the keys the new definition uses that no trigger declares yet, with the same exception.

## Examples

### A support bot called from your app

A Dynamic Input trigger declares `customer_id` as required and `channel` as optional, with `channel` defaulting to `web`.

```bash
curl -X POST https://api.seclai.com/agents/{agent_id}/runs \
  -H "X-API-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "input": "Where is my order?",
    "metadata": { "customer_id": "cus_8841", "channel": "mobile" }
  }'
```

- This run uses `channel` = `mobile`, the caller's value. A run that leaves `channel` out gets `web`.
- A run without `customer_id` is refused, as is one that sends `customerId`, a key the trigger doesn't declare.

```
Look up orders for customer {{metadata.customer_id}}.
The customer is writing from our {{metadata.channel}} app.
```

### A scheduled report

A Template Input trigger runs every Monday with this input template:

```
Write the weekly {{metadata.region}} sales summary for {{date UTC}}.
```

`region` has the default value `EMEA`. A schedule has no caller, so a scheduled trigger can't have a required key: give the key a default value instead. If a required key has no default, adding the schedule is refused. The same trigger run from the API can send `"region": "APAC"` to override the default for that run.

### Replying to an email, routed by team

An Email Received trigger declares the source keys `email_from` and `email_subject`, and a custom key `team` with the default value `billing`. Nothing sends custom keys to an email trigger, so `team` needs that default.

```
Send Email (reply_to_sender: true,
            subject: "Re: {{metadata.email_subject}}")
Prompt Call: You answer {{metadata.team}} questions for {{metadata.email_from}}.
```

### Summarizing new articles with a link back

A Content Added trigger declares the platform key `url` and the item fields `title` and `author`, ticked from the fields its items carry.

```
New post: {{metadata.title}} by {{metadata.author}}
Read it in Seclai: {{metadata.url}}
```

An item whose source has no `author` still runs; `{{metadata.author}}` reads as empty.

### Routing on a key

An If/Else step can branch on a declared key. Its condition target is `metadata.plan`, compared with `$eq` to `enterprise`:

```text
if_else (conditions: [{ target: "metadata.plan", operator: "$eq", value: "enterprise" }])
  then → Prompt Call (priority support prompt)
  else → Prompt Call (standard prompt)
```

When a run doesn't carry `plan`, the target is null and the condition takes the else branch.

### A Slack form

A Slack command trigger's form has a required **Customer** field with the key `customer` and an optional **Priority** field with the key `priority`. Both are custom keys with nothing more to declare:

```
Open a ticket for {{metadata.customer}} at {{metadata.priority}} priority.
Requested by {{metadata.slack_user_name}}.
```

`slack_user_name` is a source key, so it is declared on the trigger's Metadata step. A default value for `priority`, such as `normal`, is used when the person leaves the field blank.

### Passing metadata to another agent

A [Call Agent](https://seclai.com/docs/agent-steps/integration#call-agent-step) step sends the called agent only the keys its `metadata_mapping` names, as `{"<called agent's key>": "<this run's key>"}`. Nothing else passes through.

```text
Call Agent (agent_id: "billing-lookup",
            pass_input: true,
            metadata_mapping: { "account_id": "customer_id", "lang": "language" })
```

When this run carries `customer_id` and `language`, the called agent receives them as `account_id` and `lang`. Each must be a custom key the called agent's trigger declares, and every key it requires must be mapped, or the step can't be saved. The called agent also gets its own trigger's default values and the platform's keys for its run. An empty mapping (`{}`) sends no metadata.
