Triggers and runs
A registered workflow starts in one of two ways: another system calls a webhook trigger, or your code calls the run endpoint with a session or an org API key.
Both create a run. A run holds the input, the status, every step, and the final result.
Add a webhook trigger
Section titled “Add a webhook trigger”Open the workflow, go to the Triggers tab, and add one. You get two values:
POST https://chat.app.urai.dev/api/workflows/hooks/8Qk2...whsec-urai-vN4...The URL segment is public. The secret is the credential.
Urai shows the secret one time and stores only its hash. Nobody can recover it. If you lose it, rotate it. Rotation issues a new secret and stops the old one at once.
Default arguments
Section titled “Default arguments”A trigger can carry default arguments. Urai merges them under the caller’s payload, so the caller wins on a conflict.
{ "org_id": "org_123" }Use them for what the calling system does not know. One workflow can then serve several sources without branching on the shape of the payload.
Put nothing security-relevant here. Default arguments are defaults, not constraints. Anything a run must obey belongs in the envelope, which no caller can influence.
Run as on a trigger
Section titled “Run as on a trigger”A trigger can override the workflow’s run-as identity for the runs it starts. The same code then acts as a different identity depending on how it started.
Only someone who can manage the workflow sets this. A caller can never name an identity.
Call the webhook
Section titled “Call the webhook”Send the secret as a bearer token. The header x-urai-webhook-secret also
works.
curl -X POST https://chat.app.urai.dev/api/workflows/hooks/8Qk2... \ -H 'authorization: Bearer whsec-urai-vN4...' \ -H 'content-type: application/json' \ -d '{"org_id":"org_123"}'{ "run_id": "0f1c...", "status": "pending", "status_url": "https://chat.app.urai.dev/api/workflows/hooks/8Qk2.../runs/0f1c..."}The reply is 202 and it arrives at once. A workflow is long-running by
definition, so nothing waits for the result. Urai returns the polling URL, so
you never assemble it yourself.
The external user header
Section titled “The external user header”x-urai-external-user records an opaque identity from the calling system on the
run.
It is attribution only and grants nothing. Treat the value as supplied by an attacker, because the caller controls it.
Poll the run
Section titled “Poll the run”Use the status_url with the same secret.
curl https://chat.app.urai.dev/api/workflows/hooks/8Qk2.../runs/0f1c... \ -H 'authorization: Bearer whsec-urai-vN4...'{ "run_id": "0f1c...", "status": "completed", "result": { "chased": 4, "written_off": ["INV-2041"], "approved_by": "api_key:9c4f..." }, "error": null, "external_user_id": null, "created_at": "2026-08-11T09:14:02Z", "completed_at": "2026-08-11T09:22:40Z"}On failure, error carries the reason and result stays null.
This endpoint is scoped to the trigger that started the run. A run that someone
started from the app returns 404, and so does an unknown token. Anyone holding
a webhook secret can read every run that the secret started, so treat the secret
as read access to those results.
Start a run from your own code
Section titled “Start a run from your own code”Use the direct endpoint with a session or an org API key.
curl -X POST https://chat.app.urai.dev/api/workflows/{workflow_id}/runs \ -H "authorization: Bearer $URAI_API_KEY" \ -H 'content-type: application/json' \ -d '{"args": {"org_id": "org_123"}}'{ "id": "0f1c...", "status": "pending", "created_at": "2026-08-11T09:14:02Z" }No trigger is involved, so no default arguments apply. Pass everything the workflow needs.
This is the endpoint a scheduler calls. Read the run back with
GET /api/workflows/runs/{run_id}, which the same key can call.
An API key is a machine, so a run it starts inherits no personal identity. The run uses the workflow’s run-as setting.
Statuses
Section titled “Statuses”| Status | Meaning |
|---|---|
pending |
The run is created and not started yet. |
running |
The run is working, or waiting on a person. |
completed |
The workflow called complete(). |
failed |
A step failed and no attempts remained. |
cancelled |
Someone stopped the run. |
Two of these mislead if you read them alone:
- A run parked on an approval reports
running. That is deliberate. The run is still live, still holds its secret access, and is still yours to cancel. - A run that took a rejection branch reports
completed. The workflow ran to the end, whatever the person decided.
Branch on your own result, never on the status alone.
Watch and stop a run
Section titled “Watch and stop a run”The Runs tab lists every run with its status, input, and result. Opening one shows the state of each step, so you can see which step failed and what it returned.
Cancel a run with:
curl -X POST https://chat.app.urai.dev/api/workflows/runs/{run_id}/cancel \ -H 'authorization: Bearer <session token>'Cancelling needs a signed-in person. It stops further ticks, and the run resolves no secret from that point on, because the run record is the authority behind every secret its steps read.
It does not interrupt a step that is already running. That step finishes or fails on its own, and the run goes no further.
Things that will bite you
Section titled “Things that will bite you”A side effect in the coordinator. A fetch in run() fires again on every
tick. If something happens more times than you expect, look here first.
A caught sentinel. A try/catch around runStep(), runStepMany(),
awaitApproval(), or complete() swallows the signal that suspends the tick.
Put the try/catch inside the step.
Changed step arguments. A step is memoized by the structure of its arguments. Passing a reshaped object to a step that already finished makes it run again on the next resume.
A missing secret. A name outside the allowed secrets list, or an integration that is not connected, fails at the step. It is not a startup check. You see it in the run.