Triggering runs

A platform-managed agent can be started in six different ways. They all converge on the same executor and the same payload envelope, so your task does not care which one fired — but you do, because they have very different authentication stories.

Way inWho starts itAuth
Run NowA member, in the UIPlatform session
ScheduleThe platform's own cronNone needed
Zone appYour custom UIPer-request zone JWT
Trigger endpointAny external system you controlingest_api_key
Webhook endpointA caller that cannot send your keyThe URL itself
Agent actionAnother agent finishingInternal

1. Run Now and the input surface

Every platform-managed agent has an input surface. With no zone registered it is the built-in JSON editor at /acme/nightly-etl/input — CodeMirror, syntax-validated only, because the agent owns the shape of its own input. Three buttons: Save & Run, Save, Run (use the editor contents without persisting). After running you land on the run detail page.

This is the fastest way to prove a new agent works. Put a minimal input in the editor, press Save & Run, and watch the run.

json
{ "mode": "incremental" }

Your task reads it as payload.input.mode.

2. Schedules

All scheduling is platform-side. Do not write a Trigger.dev schedule, and do not put a Schedule Trigger node in an n8n workflow. The platform owns cron so that scheduling keeps working when a runtime is down, and so a customer can see and change it.

Go to /acme/nightly-etl/schedules/new:

Name — optional. Falls back to the cron expression in the list, so name it anyway: Nightly incremental.

Cron expression — exactly five fields, minute hour day-of-month month day-of-week. The form defaults to 0 9 * * 1-5 (weekday mornings) and has a builder if you would rather not write it by hand. Six-field expressions with seconds are rejected.

Timezone — defaults to UTC. Set it to the customer's timezone for anything a human will reason about; "nightly" means nothing without one.

Input params — a JSON object, delivered to the task as input. It belongs to this schedule alone, which is the point: two schedules on the same agent can do different work.

A scheduled run never sees the agent's saved input. It receives this schedule's params and nothing else — or {} if you left them empty. Editing the Input page has no effect on the next scheduled run, and a value you rely on there will simply be absent. Whatever a scheduled run needs must be in the schedule's own params.

0 2 * * 1-6   →  { "mode": "incremental" }    every night except Sunday
0 3 * * 0     →  { "mode": "full" }           Sunday, full reload

New schedules are created enabled. Enable, disable, and delete live on the schedules list. Each save writes a fresh input snapshot, so schedule input is fully independent of whatever is in the Input page.

An agent can have any number of schedules, and having schedules does not stop it also having a zone app — the two are independent, so one agent can be both scheduled and interactive.

What scheduling does not do for you

Four behaviours are worth knowing before you rely on a schedule:

Fires are not suppressed while a run is in flight. If the agent is still running when the next fire is due, the platform starts another run anyway — there is no in-flight check and no skip policy. For anything that writes to shared state, handle overlap yourself — see Resource knobs.

Missed fires are not backfilled. If the platform is down when a schedule was due, that occurrence is dropped. On recovery it fires once and resumes from the next slot — it does not replay the gap. An agent that must not miss a window should track its own high-water mark and catch up from it.

The minimum interval is one minute, and * * * * * is accepted.

Daylight saving shifts the time by an hour until the schedule is re-saved. The cron is converted to UTC when you save it, so a schedule set for 09:00 local keeps firing at the UTC time that was 09:00 — after a DST transition, that is 08:00 or 10:00 local. Re-saving the schedule recomputes it. For anything a person reads at a fixed local hour, put a reminder to re-save after transitions, or use UTC and accept the shift.

Any org member can create, edit, disable or delete schedules — this is not an administrator-only surface. See Roles and permissions.

Scheduled runs are platform-triggered, so they carry credentialsToken and connectionIds exactly like a manual run.

3. From your zone app

The zone triggers through the platform, using the per-request JWT it already has — no long-lived key in the zone:

ts
const cxpa = createZoneClient({
  baseUrl: process.env.PLATFORM_API_BASE_URL!,
  token: session.token,
  agentId: session.claims.agentId,
})

const { runId } = await cxpa.runs.create({ payload: { question, sessionId } })

The platform creates the run row, resolves connections, and triggers the runtime — so the run appears in history with correct attribution to the user who clicked, and your task needs no special handling. Prefer this over having the zone talk to the runtime directly. See Build the zone app.

4. The trigger endpoint

For an external system you control — a CRM, a backend job, another platform. Turn on Allow external triggers at /acme/revenue-chat/integration first; until then these endpoints return 403.

POST /api/agents/{agentId}/runs/trigger
Authorization: Bearer {ingest_api_key}

{ "input": { "question": "What drove revenue last month?" } }

→ { "runId": 123, "status": "queued" }

Fire-and-forget: the platform creates the run, triggers the runtime, and returns immediately. Via the SDK:

ts
import { triggerRun } from '@cxpa/sdk/agent'

const { runId } = await triggerRun({
  baseUrl, ingestApiKey, agentId: 42,
  input: { question: 'What drove revenue last month?' },
})

When the caller needs the answer inline

If the caller hands the output straight to a user — a generated report, a rendered document — use the synchronous variant instead of polling:

ts
const result = await triggerRunAndWait({
  baseUrl, ingestApiKey, agentId: 42,
  input: { topic: 'Q3 summary' },
  timeoutMs: 120_000,
})

if (result.timedOut) {
  // Wait window elapsed; the run continues. Poll for it.
} else if (result.status === 'completed') {
  // result.output_data has the payload.
}

The effective wait is the smallest of your timeoutMs, the agent's Max run duration, and a platform ceiling — so a 200 does not mean your requested timeout was honoured, and timedOut is a normal outcome you must handle. Use it for runs that finish in seconds or low minutes; anything longer should be asynchronous.

There is also a download variant for agents whose output is a file — it streams the bytes straight through without the platform ever storing them. See the reference.

5. Webhook endpoints, for callers who cannot send your key

Some callers cannot be told to add an Authorization header with your key — a Google Pub/Sub push subscription sends a fixed envelope and proves itself with its own Google-signed token; a third-party provider signs its payload with its own scheme. For these, enable Allow webhook triggers at /acme/revenue-chat/webhooks, which mints a webhook token on first enable.

POST /api/agents/{agentId}/runs/webhook/{webhook_token}

→ 202 { "runId": 123, "status": "queued" }

Understand the trade before using it. The platform authorises nothing here. It accepts the request and forwards body, headers, and query to your runtime verbatim; your agent decides whether the caller is legitimate. What protects the endpoint is that the URL is the credential — the token is the last path segment. So:

  • Treat the whole URL as a secret.
  • Rotating the token is the only way to revoke a leaked URL.
  • Your task must verify the caller itself.

The inbound request arrives nested under one key, so it cannot collide with your own input conventions and so a task can recognise a webhook run by shape alone:

ts
payload.input.webhook
// { method, query, headers, body, rawBody, rawBodyEncoding, receivedAt }

rawBody carries the exact bytes, which matters: verifying an HMAC signature means hashing what was signed, and re-serialising parsed JSON does not round-trip key order or whitespace. A non-JSON body is not an error — body is null and you work from rawBody.

Your runtime sees that envelope in full. What the platform stores is redacted, because the run detail page renders it to every member of the org: sensitive header values become [redacted] (names survive, so you can still debug a rejected caller) and rawBody is replaced by a byte count.

If the caller expects a meaningful response body rather than an ack — a verification handshake, a challenge echo — there is a synchronous variant, webhook-and-wait, which returns your agent's output as the response body. To control the status code or headers, return a webhookResponse object. Details in the reference.

6. Runs that start in the runtime

If execution begins somewhere the platform has no knowledge of — a workflow the customer wired up themselves, or a zone that triggers the runtime directly — the task announces itself with registerRun and receives in exchange the same credential pair a trigger payload would have carried: a credentialsToken and the connectionIds map. This is covered with the code in Write the agent task.

7. One agent triggering another

Agents can be chained without any code. Configure an action on the source agent (/acme/nightly-etl/actions) of type trigger another agent, and the source run's output becomes the target's input, unchanged. The new run is marked as action-triggered and records which run caused it.

The platform rejects any chain that would form a cycle at save time, naming the offending path, so a misconfiguration cannot produce an infinite loop.

Actions can also send email, post to Slack, or POST the output to an external URL on any run state transition. See Run lifecycle and progress.

What input your task actually receives

Each path composes it differently, and only one of them merges. Getting this wrong produces a task that works from the Input page and mysteriously receives {} on a schedule.

Started byinput the task gets
Run NowThe agent's saved input, or {}
Save & Run, a zone app, a re-run, an agent actionThe supplied payload verbatim — it replaces the saved input, it does not merge with it
A scheduleThat schedule's own params, or {}. The agent's saved input is never consulted
The trigger endpointThe agent's saved input with the caller's input spread over it

The trigger endpoint is the only merge, and it is a shallow, top-level one: a nested object in the caller's input replaces the saved nested object wholesale rather than merging into it. If the caller sends { filters: { region: 'eu' } }, every other key under filters is gone.

Choosing

  • A human decides when → Run Now, or a zone app.
  • The clock decides → a schedule.
  • Your own backend decides → the trigger endpoint with the ingest key.
  • Someone else's system decides, and it cannot hold your key → a webhook endpoint, and verify the caller in your task.
  • Another agent finishing decides → an action.

For this tutorial: nightly-etl gets two schedules, and revenue-chat is triggered from its zone app — which is what we build next.

Next

Build the zone app.