Architecture and choices

Before writing any code there are four decisions to make. Each one is recorded on the agent record and each one changes which parts of the platform you get.

1. Who controls the lifecycle?

This is the managed_by field, and it is the most consequential choice.

platform — Platform-Managedstandalone — Standalone
Who starts runsThe platform, via the runtime's REST APIYou do, however you like
Run Now buttonYesNo
SchedulesYes, owned by the platformNo — use your own cron
CancelYesNo
Input surfaceYes (zone app or JSON editor)No
Credential managementYesNo
Execution logsYes, fetched live from the runtimeNo
Run history + run detailYesYes

Choose platform unless you have a specific reason not to. It is what this tutorial builds, and it is the only mode where the platform can trigger, schedule, cancel, resolve credentials, or show logs.

Standalone: when you only need a monitoring window

Pick standalone when the agent already has a home — its own scheduler, its own triggers, its own secrets — and all you want from the platform is a shared place for the customer to see run history. The platform becomes a read-only window: your code POSTs run data to the ingest endpoint at the end of every run and the platform displays it.

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

{ "externalRunId": "run_abc123", "status": "completed",
  "startedAt": "...", "completedAt": "...", "durationMs": 68000,
  "output": { ... }, "outputs": 12 }

externalRunId is the idempotency key — call it repeatedly to move a run from running to completed. Full contract in the ingest reference, and the SDK wraps it as ingestRun().

If that is your case, most of this tutorial does not apply: you can skip credentials, triggering, schedules, and the executor entirely. You may still build a zone app for a bespoke view.

2. Which runtime?

Trigger.devn8n
Best forLong-running code, complex logic, human-in-the-loopVisual workflows, integrations assembled without code
Waitpoints (pause for a human)YesNo
LogsRich OpenTelemetry tracesAvailable, can be very large
Per-agent configtaskIdwebhookUrl + optional webhookHeaders

This tutorial uses Trigger.dev. One n8n rule is worth knowing even if you never use it: an n8n workflow on this platform must start with a Webhook Trigger node, never a Schedule Trigger — all scheduling is platform-side.

3. Zone app, or the built-in JSON editor?

Every platform-managed agent gets an input surface. There are exactly two kinds and you do not configure this on the agent — it follows from whether a zone is registered.

  • No zone registered → the platform serves a generic JSON editor at /{orgSlug}/{agentSlug}/input. Three buttons: Save & Run, Save, Run. The agent owns the shape of the JSON; the editor only validates syntax. Output is shown on the run detail page at /runs/{runId}.
  • Zone registered → a standalone Next.js app you build and deploy, served at /{orgSlug}/{agentSlug}/app and reached from a single App link in the sidebar.

Start with the JSON editor. It costs nothing, and it is enough to get the agent working end to end before you invest in UI. Add a zone when the interaction genuinely needs it — review screens, multi-step input, dashboards, conversation.

In this tutorial nightly-etl never gets a zone (it has no interactive input at all), and revenue-chat does.

These are not exclusive choices, and neither is scheduling. One agent can have a zone app and schedules and accept external triggers — the surfaces are independent, and nothing on the agent record couples them. A common shape is exactly that: an agent that ingests on a schedule and also has a zone showing the results with a "run it now" button, both reaching the same task. The tutorial keeps its two examples disjoint only to keep each one small.

4. Where does the data live?

The platform persists JSONB and nothing else. There are no file buckets and no per-agent tables. Concretely:

  • Input is a versioned JSON snapshot, linked to the run that used it.
  • Output is JSON on runs.output_data, delivered when the run completes.
  • Anything else — files, images, relational state, chat history — lives in your infrastructure, and the JSONB references it by URL or id.

So the revenue integration keeps its warehouse tables and chat history in its own database, and runs.output_data carries the answer plus enough metadata for the run detail page to be useful.

One exception worth knowing: an agent whose product is a generated file can set its output kind to file download and stream bytes straight through the platform to the caller. Even then the bytes are never stored — output_data holds only { kind, filename, mimeType, sizeBytes }.

How a run actually flows

Worth reading once before you write the task, because the payload contract falls straight out of it:

Trigger (Run Now / schedule / zone / external call)
  │
  ▼
Platform
  • creates the run row
  • looks up connection ids for the agent's credential requirements
  • signs a short-lived credentials token (60 min)
  • calls the runtime's REST API
  │        payload: { runId, input, connectionIds, credentialsToken }
  ▼
Your task
  • resolves credentials:  GET /api/credentials/{connectionId}/resolve
  •                        Authorization: Bearer {agent_api_key}
  •                        X-Credentials-Token: {credentialsToken}
  • does the work
  • returns its result, or POSTs /api/runs/{runId}/callback
  │
  ▼
Platform
  • marks the run terminal, stores output_data
  • fires any configured actions (email, Slack, webhook, another agent)

Two things to notice.

Credential values never travel in the trigger payload. What the payload carries is a connection id per credential requirement and a credentialsToken — the two halves of a claim ticket. Exchanging them for the actual secret takes a third factor the payload does not contain: the agent's own API key, which your task reads from its environment. So the secret only ever materialises inside your task's execution context, and a leaked payload alone resolves nothing.

Calling back needs no extra plumbing. The payload has no callback URL and no callback-specific token, because the callback endpoint is addressed by the runId you were given and authenticated by that same agent API key. Note that credentialsToken plays no part here — it authorises credential resolution only, which is why it can expire after 60 minutes while a long run keeps reporting progress perfectly well.

Keys, and which is which

Three different secrets show up in this tutorial. Mixing them up is the single most common source of 401s.

KeyHeld byAuthenticates
agent_api_keyYour task codeResolving credentials, and the run callback / waitpoint endpoints
ingest_api_keyWhatever starts runs from outsideTrigger, trigger-and-wait, register, ingest
zone_jwt_secretYour zone appVerifying the per-request JWT the platform mints for each visitor

Next

With those settled, check who needs to do what: Roles and permissions.