Skip to main content
An event is how you start a workflow from outside Duraton. POST /events ingests one event; Duraton matches it against every workflow trigger in the project and returns what it started. Every ingested event is also kept in a durable event log you can read and tail.
The live event log in the console

Send an event

Request body

app, runner, targetApp, dedupeId, and session are each bounded at 256 characters; a longer value, a blank name, or unparseable data returns 400. tags is bounded too: at most 20 tags per run, each key up to 64 characters matching [A-Za-z0-9_.-] and each value up to 256 characters - an over-limit tag returns 400.
tags on an event is sendable from the TypeScript SDK - SendEventInput carries a tags field - as well as over REST. The tags attach to every run the event triggers; filter those runs back with tag.<key> (see Run tags).

Unsupported characters

An event must not carry a NUL character (\u0000). Duraton’s durable event log has no representation for it, so an event carrying one could never be stored. Duraton rejects it at ingest with a 400 - before any workflow is matched or any run is started - rather than accepting the event and failing later. The check covers every text field (name, app, runner, dedupeId, targetApp, session) and the whole data payload, at any depth: a NUL inside a nested string, an array element, or even a JSON object key is rejected.
Only the escaped form \u0000 reaches this check - a raw NUL byte in the request body is already invalid JSON and is rejected as a malformed body. Either way the event is refused with a 400, and nothing is recorded.

Response

202 Accepted. The body reports what the event did:
An event that matches nothing still returns 202 with an empty triggered - it is recorded, not lost. Each triggered entry’s runId resolves to a run, and that run records the event back: its eventId is this event’s id (see runs). List every run one event fanned out with GET /runs?eventId=<id>. Cron, child (step.runWorkflow), and on-failure runs carry no eventId - they have no triggering event.

Two kinds of deduplication

Two independent mechanisms can each report deduped: true, and they mean different things. Knowing which one fired matters, because one throws the event away and the other only suppresses a single run. In short: dedupeId is event-level and total - the exact same event is collapsed to a no-op, nothing is recorded, and nothing is woken. It is the safety net for an at-least-once caller retrying POST /events. idempotency is run-level and partial - the event still lands, still wakes waiters, and still runs every other matching workflow; only a second run of the idempotent workflow for the same derived key is suppressed within the window. Because both surface deduped: true, tell them apart by the rest of the response: an event-level drop returns no triggered array and no runId, whereas an idempotency drop still carries the event’s triggered fan-out with deduped set on the affected entry.

Reading the log

The log records every event with what it did, newest first.
Each record carries the event (name, app, data), its source (api for an external POST, emit for a workflow’s step.emit), when it arrived, and the triggered fan-out:
The stream is a best-effort live view - a slow or reconnecting client can miss events. GET /events is the complete record.