Triggers and wakes
Start an Agent's turns on a schedule, at a time, from a signed webhook, or when the agent books its own next turn.
{
"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
}npx nylorun apply
npx nylorun agents triggers list teams/support/triageTriggers start an Agent's turns with no client connected.
They live in the runtime section of its
agent manifest, 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), the folder's
runtime.json, as above, saved with nylorun apply, or the built agent:
// 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
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. |
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. |
prompt | The message each run sends. |
The Runtime checks triggers when the manifest is saved, against the Tenant's
Runtime 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
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. 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.
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
Each time a trigger fires, or is run by hand, is a run:
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);npx nylorun agents runs teams/support/triage --trigger hourly-checkstatus | 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
scheduleorattrigger; its later fires are skipped asoverlap. - 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 andtriggerJitterMaxMs(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.
Status
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, 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
promptorsessionapplies 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
and List an Agent's runs.
In Studio, 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
A webhook trigger has an endpoint, POST /v1/webhooks/{endpointId}, that takes
Standard Webhooks deliveries with no
credential and no Nylorun-Protocol header: the endpoint id and the signature
are the grant. Make its signing secret first:
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.
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:
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.
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.
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
Triggers run turns with nobody watching. An hourly trigger on 100 Agents is
2,400 turns a day. Set model budgets
before you leave Agents running unattended, and keep triggerRunsPerHour low
enough that an Agent that keeps waking itself stops.