Troubleshooting

Grouped by where the symptom shows up. Most of these fail in a way that points at the wrong thing, which is why they are worth reading before you hit them.

Runs

A run fails immediately with "task not found".

Three causes, in order of likelihood. The agent's Task ID does not match the task's id string. Or the task was never deployed to the environment the platform's runtime environment key belongs to — a prod key cannot see a task deployed only to staging. Or you are pointed at Development and trigger dev is not running.

A run triggered in development just sits in queued forever.

trigger dev is not running, or it crashed. Development runs execute on your machine; with nothing attached, they queue silently and never error. Keep that terminal visible.

The runtime shows the run succeeded, but the platform still shows it running.

Up to one poll interval of lag is normal — the platform notices completion on its next tick, 60 seconds later by default. If it persists well beyond that, the platform cannot read the run's status: check that the runtime environment's secret key still works, since polling is what closes out a run that never calls back. Lower the agent's poll interval, or call cxpa.complete(), if the delay itself is the problem. See Write the agent task.

The run reaches completed but output_data is empty.

The platform writes the task's return value, but only when it is an object with at least one key — an empty object is left as null rather than overwriting with {}. A task that returns nothing, a bare string, or a number records no output. Return an object.

outputs is always zero on the metrics page.

outputs is read as a top-level integer on the returned value (or from complete({ outputs })). Nesting it inside another object, or returning it as a string, leaves it unset.

The task cannot find its input.

Domain input is nested: payload.input.question, not payload.question. The platform wraps whatever came from the input surface, the schedule, or the trigger call under input.

A run is marked failed but the error is empty.

The task threw without reporting. Wrap the body and call cxpa.fail({ message }) before rethrowing, so the platform has something to show. Bare throws are visible in the runtime's logs but not on the run record.

An hour-long task retries after a validation failure.

Use AbortTaskRunError for failures a retry cannot fix — bad input, reconciliation breaches. A plain throw consumes retry attempts and delays the alert by however long the task takes.

A file the task reads works in dev and throws ENOENT in production.

It is not in additionalFiles in trigger.config.ts. Trigger.dev bundles your code; anything read at runtime that is not code must be listed there.

Two ETL runs overlapped and corrupted data.

Nothing prevents overlap: a schedule fires even while the previous run is still going, and a manual Run Now can land on top of either. Serialise inside the task — take a lock and return early if you don't get it — and dedupe on a natural key. Do not reach for a runtime queue; see the next entry. Full pattern in Write the agent task.

A run failed with "Run never started (queued timeout exceeded)" — and then ran anyway.

The run sat in the runtime's queue for more than 10 minutes, so the platform gave up on it. Almost always this means the task sets a runtime concurrency limit (queue: { concurrencyLimit: 1 }) and a previous run held the slot. The runtime kept the run and executed it when the slot freed, which is why the work happened despite the failure.

The 10-minute ceiling is measured from run creation, is not configurable per agent, and is not extended by the agent's Max run duration. Remove the runtime queue and serialise inside the task instead.

Scheduled runs fire at the wrong time.

Check the schedule's timezone — it defaults to UTC, which is rarely what "nightly" means to a customer. Also confirm the cron has exactly five fields; six-field expressions with seconds are rejected at save time.

If it is exactly an hour off, it is daylight saving: the cron was converted to UTC when the schedule was saved and does not follow later transitions. Re-save the schedule to recompute it.

A scheduled run was missed entirely and never caught up.

Missed occurrences are dropped, not replayed — on recovery the schedule fires once and continues from the next slot. An agent that must not lose a window needs to track its own high-water mark and catch up from it.

Triggering from outside

The trigger endpoint returns 403.

Allow external triggers is off. Turn it on at /{orgSlug}/{agentSlug}/integration. The same switch gates trigger, trigger-and-wait, and register.

The trigger endpoint returns 401.

You are sending the wrong key. /trigger, /trigger-and-wait, /register, and /ingest want the ingest key; /resolve and /callback want the agent key. They are different values on different pages.

The agent will not run at all, from anywhere.

Its status is discovery. That is the intended behaviour — discovery agents refuse to execute. Flip it to active at /{orgSlug}/{agentSlug}/settings.

trigger-and-wait returned 200 but the run is not finished.

Check timedOut. The effective wait is the smallest of your timeoutMs, the agent's Max run duration, and a platform ceiling, so your requested timeout is an upper bound, not a guarantee. A timed-out response is a normal outcome — the run continues, and you fall back to polling.

A webhook caller gets 401 on a URL that looks right.

A bad token and a nonexistent agent return an identical 401 by design, so the endpoint cannot be used to probe which agent ids exist. Re-copy the full URL from the webhooks page — the token is the last path segment, and the whole URL is the credential.

Credentials

resolveCredential returns 404.

The connection id is not visible to this agent — usually the requirement was never connected by a member, so it is absent from connectionIds entirely and you passed undefined. Check the credentials page shows it as connected.

resolveCredential returns 403.

The credentials token is bound to a different agent or run. Almost always a copied environment variable: one agent's API key in another agent's runtime. Keys are scoped per agent.

resolveCredential returns 401 partway through a long run.

The credentials token expired — it has a 60-minute TTL, measured from the start of the run. The fix is not to refresh it (there is no refresh); it is to resolve everything up front and keep the values in memory. The TTL bounds when /resolve may be called, not how long the run may last, so a twelve-hour task is fine as long as it resolved in the first minute. Resolve before a branch even if that branch might not need the credential. See Credentials.

The task throws "No connection for …" before making any request.

The lookup by integration id missed, so connectionIds had no entry for it. Either the requirement was renamed on the platform and the code still uses the old id, or a member never connected it. Compare the string in your code against the requirement's Integration ID on the platform — they must match exactly.

A run needs credentials but the payload has no credentialsToken or connectionIds.

The two travel together on a platform-triggered run, so both missing points at one of two causes.

The agent has no active connections. The platform builds the connection map first and only mints a token when the map is non-empty — an agent with nothing connected receives neither. Connect the credential requirements and trigger again.

The run was not triggered by the platform. A task invoked directly on the runtime never receives either. Trigger it from the platform — Run Now, the input surface, or a zone app calling the platform — or, if execution genuinely starts on the runtime, call registerRun, whose response carries the same pair. See Credentials.

OAuth is greyed out when declaring a credential requirement.

The agent has no Nango environment selected. The reason is shown inline. Set it in the agent's settings.

Zone app

Every zone page returns 200 without a token — the zone is publicly readable.

The middleware matcher is wrong. With basePath configured, Next.js matches the path after stripping the base path, so a matcher written as /:orgSlug/:agentSlug/app/:path* matches nothing and the middleware never runs. It must be ['/:path*']. This fails open, so it will not announce itself — test it with curl against the zone's own origin and expect a 401.

Every zone request returns 401.

Either ZONE_JWT_SECRET does not match the platform's value for this agent (signature invalid), or AUDIENCE_ID does not match the registry audience (signature valid, audience rejected). They are different failures with the same status code; check the audience first, since it is the one people assume is derived automatically. It is not.

The zone renders as unstyled HTML — every _next/* request 404s.

The platform's asset rewrites are generated at build time. Adding ZONE_*_URL without redeploying the platform without build cache leaves the rewrite absent. Redeploy with the cache unchecked.

_next/* requests loop with ERR_TOO_MANY_REDIRECTS.

Vercel Skew Protection is on. Turn it off on both the zone and the platform project. If instead the requests redirect to a Vercel SSO page, it is Deployment Protection — set it to preview-only or off.

Every request 404s but the build succeeded.

ZONE_ORG_SLUG or ZONE_AGENT_SLUG does not match the pair the platform registry binds this zone to. They are compiled into basePath, so a rebuild is required after fixing them — a restart is not enough.

Server actions fail as cross-origin.

PLATFORM_ORIGIN is missing, wrong, or has a trailing slash. It must be in experimental.serverActions.allowedOrigins, and it is read at build time.

A client fetch('/api/...') hits the platform instead of the zone.

That is expected — the browser is on the platform's origin. Zones do not use route handlers; move the call into a server action.

Navigation produces doubled paths like /acme/revenue-chat/app/acme/revenue-chat/app/chat.

Next.js already prepends basePath to Link, redirect, and router.push. Write paths as if the zone were at the root.

/{orgSlug}/{agentSlug}/zone-jwt-secret says "Not set".

The registry entry was added after the agent record was created, so nothing was minted. Press Rotate Secret and set the new value in the zone.

The App link is missing from the sidebar.

There is no registry entry for this (org, agent) pair, or its URL variable is unset. A falsy URL disables the zone deliberately, and the agent falls back to the built-in JSON editor.

Rate limits

Progress reporting starts failing with 429 partway through a run.

The callback endpoint allows 20 requests per minute per agent, shared with waitpoint completions, and a task calling cxpa.output() once per processed item exceeds it on any real batch. The run itself is unaffected — only the reporting fails — so the symptom is a run that completes with gaps in its progress. Batch to every N items or every few seconds, and move fine-grained progress to runtime metadata, which is not rate-limited.

A retry loop makes it worse. Counting is per fixed one-minute bucket, so retrying immediately spends the next bucket too. The 429 carries Retry-After with the seconds until the bucket resets — honour it.

An external system gets 429 on /trigger. 20/min per agent, and the synchronous variants are stricter still at 5/min because each holds a connection open. If a caller legitimately needs more, batch the work into fewer runs rather than raising the trigger rate. Full table in Limits and quotas.

Waitpoints and human-in-the-loop

A resumed run fails immediately with "Run exceeded running timeout".

The pause counted. A run's elapsed time is measured from when it first started and that stamp is never reset, so a run that waited three days resumes three days over its Max run duration and is failed on the next poll tick. The cap is 24 hours, so no setting saves a multi-day pause. Do not model long approvals as a paused run — see Run lifecycle and progress.

resolveCredential returns 401 right after a waitpoint resumes.

The credentials token expired during the pause — it is valid 60 minutes from the start of the run, not from the resume. A credential resolved before the pause may also have expired on the provider's side. Either re-mint with registerRun (needs Allow external triggers and the ingest key), or restructure so the post-approval work happens in a new run.

Nobody knows the run is waiting.

Waiting runs notify no one by default. Configure an email action on the waiting state with the reviewers in its recipient list, and set the optional url when pausing so the notification links to your review screen rather than the generic run page.

Someone who should not have approved it, did.

Expected: the platform lets any org member, in either role, complete any waiting run, and the userId field is attribution for the audit log only — it authorises nothing. Restricting the approver is your code's job.

A run sits in waiting forever and the timeout never fires.

The waitpoint timeout is enforced by the runtime, not the platform, so it only exists if your task set one when creating the token. Without it, the run waits until someone acts or the runtime's own limits intervene.

Zone data and tenancy

A user sees another organization's rows.

Almost always a shared database with an unfiltered query. The platform binds a zone deployment to one organization; it does nothing to your database. If several deployments share one, every read and write needs an org_id predicate from claims.orgId — including counts, aggregates and any "show everything" view. See Zone data and tenancy.

A user sees another user's rows in the same org.

Expected unless you filtered. Every member of the bound org reaches the zone with a valid token; user-level scoping is entirely yours.

A database call from the zone fails or exposes a key.

The database client must be constructed server-side only — in a server action or async server component. A client component importing it ships the credential to the browser. There is no row-level security to fall back on, because the platform's JWT is not a database identity.

runs.get(runId) returns a run the current user should not see.

Run reads are scoped per agent, not per user: any zone user can fetch any run belonging to the bound agent. Check ownership against your own records before rendering one.

Still stuck

Narrow it to a side first — that halves the search space:

  • Does Run Now from the platform reach completed? If yes, the platform, runtime credentials, task id, and credentials all work, and the problem is in whatever was triggering it.
  • Does the run appear in the runtime's own dashboard? If not, the platform never reached the runtime — look at the runtime environment's key and the task id. If it does, the platform reached it and the problem is in the task or the callback.
  • Does curl against the zone's own origin return 401? If not, fix the matcher before debugging anything else.

The reference sections have the exact wire formats when you need to compare byte for byte: API Reference for status codes, SDK for method signatures, Zone App Guide for zone deployment detail.