# Triggers and wakes (/docs/run/triggers)



```json title="runtime.json"
{
  "kind": "none",
  "triggers": {
    "hourly-check": { "schedule": "0 * * * *", "timezone": "Asia/Kolkata", "session": "triage-inbox", "prompt": "Check for new issues." },
    "alerts": { "webhook": "standard-webhooks", "session": "new", "prompt": "An alert arrived. Investigate it." },
    "launch-day": { "at": "2027-03-01T09:00:00+05:30", "session": "launch", "prompt": "Post the launch checklist." }
  },
  "selfWake": true
}
```

```sh
npx nylorun apply
npx nylorun agents triggers list teams/support/triage
```

Triggers start an [Agent's](/docs/run/agents) turns with no client connected.
They live in the runtime section of its
[agent manifest](/docs/run/agent-manifests), by id, so every save path carries
them: a file per trigger in an agent manifest's `triggers/`, whose body is the
prompt (see [Agent manifests](/docs/build/agent-manifests#triggers)), the folder's
`runtime.json`, as above, saved with `nylorun apply`, or the built agent:

```ts
// triage is the Agent() from agents/triage/agent.ts.
await client.agentManifests.save({
  agent: triage,
  runtime: {
    kind: "none",
    triggers: {
      "hourly-check": { schedule: "0 * * * *", timezone: "Asia/Kolkata", session: "triage-inbox", prompt: "Check for new issues." },
      alerts: { webhook: "standard-webhooks", session: "new", prompt: "An alert arrived. Investigate it." },
    },
    selfWake: true,
  },
});
await client.agents.put("teams/support/triage", { manifest: "triage" });
```

Each Agent made from the manifest has its own copy of every trigger: its
status, its next run, and its runs. A turn a trigger starts is an ordinary turn
on an ordinary session, with the same model, tools, tool policy, and secrets as
any other.

## Triggers [#triggers]

A trigger id is 1 to 64 letters, digits, `.`, `_`, and `-`, starting with a
letter or digit. Each trigger has exactly one of:

| Field      | Fires                                                                                                                                                         |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `schedule` | On a five-field cron (minute, hour, day of month, month, day of week), in `timezone`, an IANA name. `timezone` defaults to UTC and goes only with `schedule`. |
| `at`       | Once, at an RFC 3339 time with an offset. It must be in the future when the manifest is saved.                                                                |
| `webhook`  | When a signed delivery reaches its endpoint. The only scheme is `"standard-webhooks"`; see [Webhooks](#webhooks).                                             |

And both of:

| Field     | Meaning                                                                                                        |
| --------- | -------------------------------------------------------------------------------------------------------------- |
| `session` | Where its runs go: a session id of the Agent, or `"new"` for a new session per run. See [Sessions](#sessions). |
| `prompt`  | The message each run sends.                                                                                    |

The Runtime checks triggers when the manifest is saved, against the Tenant's
[Runtime limits](/docs/run/limits): at most `triggersPerAgent` (20) per
manifest, two runs of one schedule at least `triggerMinIntervalMs` (1 minute)
apart, and a prompt of at most `triggerPromptChars` (8,000) characters. A save
that breaks one is refused with the trigger's path and the limit.

## Sessions [#sessions]

A named `session` is a session id of the Agent. The trigger's first run opens
it, owned by the Agent's `owner`, or by `installation` when the Agent has no
owner guard. After that it is a session like any other: people and apps can
read it, send to it, and [follow its events](/docs/run/events). The Runtime
keeps no main session; to have the trigger and a person share one conversation,
name the same session id.

`"new"` opens a session per run, named `run-<runId>`.

A named session that belongs to another Agent fails the run with
`session_conflict`. A run's message goes through the session's queue: when a
turn is running there, the message waits and starts the next turn. See
[Messages while a turn runs](/docs/run/sessions#messages-while-a-turn-runs).

The run's message is `command.message` with `cause`
(`{ kind: "trigger", triggerId, runId }`), written by `core` for the trigger,
so your app can tell a triggered turn from a person's.

## Runs [#runs]

Each time a trigger fires, or is run by hand, is a run:

```ts
const page = await client.agents.runs("teams/support/triage", { triggerId: "hourly-check", limit: 20 });
for (const run of page.runs) console.log(run.firedAt, run.status, run.reason, run.late, run.sessionId);
```

```sh
npx nylorun agents runs teams/support/triage --trigger hourly-check
```

| `status`  | Meaning                                                                   | `reason`                                                                                                                                                                                                      |
| --------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `started` | It started a turn (`turnId`).                                             |                                                                                                                                                                                                               |
| `queued`  | A turn was running on the session; the message waits (`queuedMessageId`). |                                                                                                                                                                                                               |
| `skipped` | Nothing started.                                                          | `overlap`: the session's queue already holds this schedule's last run. `rate_limited`: the Agent's triggers started or queued `triggerRunsPerHour` (60) runs in the last hour. `paused`: the Agent is paused. |
| `failed`  | The run could not reach the session.                                      | `queue_full`, `session_conflict`, `agent_unavailable`, or another session error code.                                                                                                                         |

* **After downtime** a trigger fires once, marked `late`, and never backfills
  the times it missed. A Tenant that is stopped fires nothing.
* **A busy session** gets at most one queued run per `schedule` or `at`
  trigger; its later fires are skipped as `overlap`.
* **A fixed offset.** A schedule's runs are moved by an offset derived from
  the Agent and trigger id, at most `triggerJitterPercent` (10%) of the
  interval and `triggerJitterMaxMs` (5 minutes), so a fleet of hourly Agents
  does not fire in the same second. The offset never changes, so runs do not
  drift.
* **Runs are kept** for `triggerRunRetentionMs` (30 days).

Each run also writes `agent.run` (`runId`, `triggerId`, `dueAt`, `late`,
`status`, `reason`, `sessionId`) on the Agent's stream; see
[Agent payloads](/docs/run/events#agent-payloads).

## Status [#status]

```ts
const id = "teams/support/triage";
for (const trigger of await client.agents.triggers(id)) {
  console.log(trigger.id, trigger.kind, trigger.status, trigger.pausedReason, trigger.nextRunAt);
}

await client.agents.pauseTrigger(id, "hourly-check");
await client.agents.resumeTrigger(id, "hourly-check");
const run = await client.agents.runTrigger(id, "hourly-check", { idempotencyKey: "deploy-42" });
```

| `status`    | Meaning                                                                                                           |
| ----------- | ----------------------------------------------------------------------------------------------------------------- |
| `active`    | It fires. `nextRunAt` says when.                                                                                  |
| `paused`    | It does not fire. `pausedReason` is `manual` (paused by a call) or `new` (a version the Agent moved to added it). |
| `completed` | An `at` trigger that ran.                                                                                         |

Every trigger starts active when an Agent is made. When an Agent
[moves to a new version](/docs/run/agents#versions), its triggers merge by id:

* a trigger the new version adds arrives `paused` (`new`): resume it to turn it
  on;
* a trigger it drops is removed (its runs stay);
* a trigger whose timing changed gets its next time from the new timing;
* a changed `prompt` or `session` applies at the next fire.

A webhook trigger keeps its endpoint and signing secret across moves.

| Call                                                        | CLI                                                          | Does                                                                                                                                                       |
| ----------------------------------------------------------- | ------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `client.agents.triggers(id)`                                | `agents triggers list <id>`                                  | Each trigger: `kind`, `status`, `pausedReason`, its timing, `session`, `nextRunAt`, `lastRunAt`, and for a webhook `webhookUrl` and `secretSet`.           |
| `client.agents.pauseTrigger(id, trigger)`                   | `agents triggers pause <id> <trigger>`                       | Stop it firing. A run already started or queued goes on.                                                                                                   |
| `client.agents.resumeTrigger(id, trigger)`                  | `agents triggers resume <id> <trigger>`                      | Fire again from its next time after now; missed times are not run. An `at` whose time has passed is `409 trigger_expired`: save a version with a new time. |
| `client.agents.runTrigger(id, trigger, { idempotencyKey })` | `agents triggers run <id> <trigger> [--idempotency-key <k>]` | Run it once now, paused or not, as if it fired. A retry with the same key answers the same run.                                                            |
| `client.agents.rotateWebhookSecret(id, trigger)`            | `agents triggers new-secret <id> <trigger>`                  | A webhook trigger's new signing secret.                                                                                                                    |
| `client.agents.runs(id, { triggerId?, limit?, cursor? })`   | `agents runs <id> [--trigger <id>]`                          | Runs, newest first.                                                                                                                                        |

The Agent view's `nextWakeAt` is the earliest next run of its active triggers
and booked wakes. Reading triggers and runs takes `agents:read`,
`agents:write`, or `sessions:own` for an Agent the caller reaches; pausing,
resuming, running, and new secrets take `agents:write`. Request and response shapes are on
[List an Agent's triggers](/reference/runtime/agents/triggers/list-an-agents-triggers)
and [List an Agent's runs](/reference/runtime/agents/triggers/list-an-agents-runs).

In [Studio](/docs/run/studio#agent-page), the Agent page's **Triggers** tab
lists each trigger with its schedule in words, the session its runs go to, its
next run, and its status, with **Run now**, **Pause**, **Resume**, and **Copy
webhook URL**. Below them, every run, newest first. The turns triggers start
show as ordinary sessions of the Agent.

## Webhooks [#webhooks]

A webhook trigger has an endpoint, `POST /v1/webhooks/{endpointId}`, that takes
[Standard Webhooks](https://www.standardwebhooks.com) deliveries with no
credential and no `Nylorun-Protocol` header: the endpoint id and the signature
are the grant. Make its signing secret first:

```ts
const { webhookUrl, secret } = await client.agents.rotateWebhookSecret("teams/support/triage", "alerts");
```

The secret (`whsec_…`) is shown this once, and replaces the one before at
once. Give the sender the secret and the endpoint's path.

`webhookUrl` starts with the Runtime's public URL, `NYLORUN_PUBLIC_URL`.
Behind a reverse proxy, start the Runtime with it set to the proxy's URL; see
[Reverse proxy](/docs/deploy/vm#reverse-proxy).

A delivery:

| Check                                                                                                                                 | Answer                                                                                     |
| ------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ |
| `webhook-id`, `webhook-timestamp`, and `webhook-signature` present, the timestamp within 5 minutes, signed with the endpoint's secret | Otherwise `401`, and nothing is written.                                                   |
| The body, any media type, at most `webhookBodyBytes` (256 KiB)                                                                        | Otherwise `413`. The body is read and verified before anything else.                       |
| The trigger is active                                                                                                                 | Otherwise `409 trigger_paused`, and nothing starts.                                        |
| A new `webhook-id`                                                                                                                    | `202 { runId, status, reason }`. A retry with the same `webhook-id` answers the first run. |

GitHub, Stripe, and Slack sign their own way: receive them in your app and
relay each delivery, signed with the trigger's secret:

```ts
import { createHmac, randomUUID } from "node:crypto";

export async function deliver(url: string, secret: string, body: string) {
  const id = `msg_${randomUUID()}`;
  const timestamp = Math.floor(Date.now() / 1000).toString();
  const key = Buffer.from(secret.slice("whsec_".length), "base64");
  const signature = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest("base64");
  const response = await fetch(url, {
    method: "POST",
    headers: {
      "content-type": "application/json",
      "webhook-id": id,
      "webhook-timestamp": timestamp,
      "webhook-signature": `v1,${signature}`,
    },
    body,
  });
  return (await response.json()) as { runId: string; status: string; reason: string | null };
}
```

The run's message is the trigger's `prompt`, then the body in a
`<trigger-payload>` block labelled as data from an outside sender that cannot
approve anything. The label names the data; it does not stop a model from
following it. Hold risky tools with the [tool policy](/docs/run/tool-policy).

The webhook path is the only Runtime route a sender reaches without a key. Put
a Runtime that faces the internet behind a proxy that limits outside requests
to `/v1/webhooks/`; see [Reverse proxy](/docs/deploy/vm#reverse-proxy).

## Wakes [#wakes]

`"selfWake": true` in the runtime section offers the agent `schedule_wake`:
it books its own session's next turn, to check on something later or keep a
routine going.

| Input    | Meaning                                                                                          |
| -------- | ------------------------------------------------------------------------------------------------ |
| `in`     | How long from now: a whole number and `s`, `m`, `h`, or `d`, such as `30m`, `2h`, `1d`.          |
| `at`     | An RFC 3339 time with an offset. Give exactly one of `in` and `at`.                              |
| `note`   | The booked turn's message, in the agent's own words. At most `wakeNoteChars` (2,000) characters. |
| `cancel` | `true` removes the booking instead.                                                              |

A session holds one booking: a new one replaces it. A wake is at least
`triggerMinIntervalMs` (1 minute) and at most `wakeMaxAheadMs` (30 days)
ahead. When it is due, the note starts a turn through the session's queue,
with `cause.kind: "self"`, and the booking is gone; the agent books again if it
wants another.

The session records `wake.booked` (`at`, `note`, `replaced`) and
`wake.cancelled`. The call itself is a `tool.completed` with `kind: "runtime"`.
A wake's run is listed with the Agent's runs, with `kind: "wake"`.

## Cost [#cost]

Triggers run turns with nobody watching. An hourly trigger on 100 Agents is
2,400 turns a day. Set [model budgets](/docs/run/models#usage-and-budgets)
before you leave Agents running unattended, and keep `triggerRunsPerHour` low
enough that an Agent that keeps waking itself stops.

## Next step [#next-step]

<Cards>
  <Card title="Agent memory" description="Keep what a triggered turn learns for the next one." href="/docs/run/memory" />

  <Card title="Runtime limits" description="Change the trigger, wake, and webhook limits." href="/docs/run/limits" />
</Cards>
