- A trigger is the immutable audit anchor for the triggering event.
- A batch is the rollup for that one trigger — aggregate metadata and counts, one per triggering event.
- A job is one record. Every record you trigger gets its own job carrying that record’s resolved
input_data. This is where per-record inputs and status live. - An execution is the durable run that actually processes one job.
Batch vs. Job. A batch is the whole triggering event (the rollup); a job is a single record inside it. Per-record inputs live on jobs, not on the batch — the batch carries
record_count, not the record array. To read the records of a batch, list its jobs with GET /flows/jobs?batch_id=<batch_id>.Batches
A batch represents one triggering event and groups every execution it produced. Key fields:trigger_type and trigger_source are two independent dimensions. Type is the deterministic entry point (api_call / webhook / schedule / event); source is the free-form caller label you pass at trigger time. Filter or group on either one. See Triggers for the full model.Batch status lifecycle
A batch moves
pending → running → completed | partially_failed | failed. Passing scheduled_at at trigger time inserts a scheduled stage before running. On the bulk path the terminal state is decided by the import counters instead, which is where completed_with_errors comes from.
Any batch that has not already finished can be cancelled with POST /flows/batches/{batch_id}/cancel — including one that is actively running. A batch that has already completed, failed, or been cancelled returns 409. Cancelling needs no special role: any key for the owning organization can cancel that organization’s own batches.
DELETE /flows/batches/{batch_id} is the deprecated spelling of the same cancellation and additionally requires an admin role. Use the POST .../cancel verb; the DELETE form is kept only for existing integrations.pending while its rows are being bound, then flips to queued once every row has both a job and an execution. From queued, the batch’s delivery is paced against your Domain’s rate budget — sent as fast as the provider’s limits allow and never faster.
Account-wide runs feed
GET /flows/batches lists every batch across all of your definitions, newest first — the backing feed for a “recent runs” screen. GET /flows/definitions/{id}/batches is the same feed scoped to a single definition.
The response is the converged
Page envelope — items, total, limit, skip, and next_cursor. This is a data-family feed, so total is always null; page forward until next_cursor is null, which signals no more rows. Only your own account’s batches are ever returned.
Jobs
A job is one record inside a batch. When you trigger with a single input you get one job; when you trigger withrecords you get one job per record. The job is the per-record audit point — it holds the resolved input_data for that record and the per-record status. Today each job maps 1:1 to one execution.
Job status lifecycle
On the immediate path jobs are written straight to
started — there is no pending window. When you defer a batch with scheduled_at, its jobs are created pending until the batch runs. The terminal status (completed / failed) mirrors the job’s execution: the execution is the source of truth, and the job’s terminal state is set to match. Cancelling a batch flips its non-terminal jobs to cancelled, except on the bulk path, where rows that were never sent settle as unprocessed.The last four are import outcomes, not execution outcomes. A bulk row ends at the answer your platform gave for that record —
accepted, rejected, unprocessed or unresolved — while completed / failed continue to describe the per-record execution path exactly as before. Import outcomes covers what each one means, why a rejection carries both the provider’s own text and a normalized class, and how the four roll up into a batch’s terminal state.Jobs on the bulk path
Uploading a record set does not change the shape of the chain. Each uploaded row is one job and one execution — the 1:1 relationship is exactly the same as on the inline path, at any volume. What differs is where the row’s data lives. On the bulk path the record is stored only on the job, as itsinput_data; the execution references its job by job_id rather than carrying a second copy of the record. So to read what was uploaded, read the job.
A row’s own lifecycle before it starts is staged → queued: staged while it belongs to a record set that has not been triggered, queued once a trigger has bound it to a batch and given it an execution. From queued the row is dispatched to your platform and ends at the answer that platform gave for it — accepted, rejected, unprocessed or unresolved. See Import outcomes.
Listing jobs
GET /flows/jobs lists every job across all of your definitions, newest first. Scope to one batch with GET /flows/jobs?batch_id=<batch_id> — this is how you read the per-record inputs that used to be embedded on the batch.
The response is the converged
Page envelope (items, total, limit, skip, next_cursor). Like the batches feed, this is a data-family feed, so total is always null; page forward until next_cursor is null. Fetch one job with GET /flows/jobs/{job_id}; a missing or cross-tenant job returns 404.
Executions
An execution is one durable run processing one set of inputs. Each belongs to exactly one batch (and, 1:1, one job):Execution status lifecycle
An execution moves
pending → running → completed | failed, with any number of running → waiting → running excursions along the way.
waiting is what a long pause looks like from the outside. A signaled task or a send-and-wait can be parked for hours or days, and before this status existed such a run reported running the whole time — indistinguishable from one that was actually doing something.Two consequences worth knowing. started_at is not re-stamped on resume, so a two-day wait reads as a two-day run rather than a two-second one. And cancelling a batch settles its waiting executions like any other non-terminal state — a parked execution is never stranded under a cancelled batch.GET /flows/batches/{batch_id}/executions and fetch one with GET /flows/executions/{id}. That listing’s status filter accepts waiting like any other status, so “which of this batch’s runs are parked” is one query.
Per-step run detail
GET /flows/executions/{id} embeds a per-step breakdown so a run-detail screen renders in one call — the step list is bounded by the definition’s task chain, so there is no pagination and no per-step endpoint.
Two fields carry it:
steps_source is always set on the detail route so a client can distinguish “this run genuinely has no steps” from “step detail isn’t available for this run”:
steps and steps_source are populated only on the single-execution detail route (GET /flows/executions/{id}). The executions list route leaves them null — it isn’t the run-detail consumer.Single-record vs. multi-record triggers
A trigger creates jobs (and their executions) in one of two shapes:- Single-record — pass
input_data. The batch hasrecord_count: 1and one job that receives yourinput_data, which runs as one execution. - Multi-record (fan-out) — pass a
recordsarray (up to 10,000 records inline). One job is created per record, each carrying that record as itsinput_data, and each job runs its own execution. The batch’srecord_countis the array length, and its final status aggregates all children: all succeed →completed, all fail →failed, a mix →partially_failed. Read the individual records withGET /flows/jobs?batch_id=<batch_id>. - Bulk (by reference) — beyond 10,000 records, upload a record set and pass
record_set_idinstead ofrecords. The shape is identical (one job and one execution per row); only the delivery of the records into the platform differs. Up to 5,000,000 records per set.
Recurring triggers: schedules
To run a definition on a repeating cadence, create a schedule withPOST /flows/schedules. Each fire creates a normal batch — visible under GET /flows/definitions/{id}/batches with trigger_type: "schedule" and a schedule_id pointing back at the schedule — so the batch and execution lifecycle above applies unchanged.
Fire behavior is fixed: if a previous fire is still running when the next tick arrives, that tick is skipped, not queued. Missed fires during downtime are replayed only within a 60-second catch-up window.
Manage schedules with
PATCH /flows/schedules/{id} (change the cron expression, pause/resume with enabled, update input_data) and DELETE /flows/schedules/{id} — deleting a schedule never affects batches it already fired.