NylorunDocsBeta
RunAgents

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.

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
}
npx nylorun apply
npx nylorun agents triggers list teams/support/triage

Triggers 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:

FieldFires
scheduleOn 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.
atOnce, at an RFC 3339 time with an offset. It must be in the future when the manifest is saved.
webhookWhen a signed delivery reaches its endpoint. The only scheme is "standard-webhooks"; see Webhooks.

And both of:

FieldMeaning
sessionWhere its runs go: a session id of the Agent, or "new" for a new session per run. See Sessions.
promptThe 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-check
statusMeaningreason
startedIt started a turn (turnId).
queuedA turn was running on the session; the message waits (queuedMessageId).
skippedNothing 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.
failedThe 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.

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" });
statusMeaning
activeIt fires. nextRunAt says when.
pausedIt does not fire. pausedReason is manual (paused by a call) or new (a version the Agent moved to added it).
completedAn 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 prompt or session applies at the next fire.

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

CallCLIDoes
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:

CheckAnswer
webhook-id, webhook-timestamp, and webhook-signature present, the timestamp within 5 minutes, signed with the endpoint's secretOtherwise 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 activeOtherwise 409 trigger_paused, and nothing starts.
A new webhook-id202 { 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.

InputMeaning
inHow long from now: a whole number and s, m, h, or d, such as 30m, 2h, 1d.
atAn RFC 3339 time with an offset. Give exactly one of in and at.
noteThe booked turn's message, in the agent's own words. At most wakeNoteChars (2,000) characters.
canceltrue 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.

Next step

On this page