`GET /api/workflows/board?project_id=<uuid>` — one server-paginated, tab/ trigger/status-filtered page of a project's workflows + total + NavTab facets.
const url = 'https://example.com/api/workflows/board?project_id=2489E9AD-2EE2-8E00-8EC9-32D5F69181C0';const options = {method: 'GET'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request GET \ --url 'https://example.com/api/workflows/board?project_id=2489E9AD-2EE2-8E00-8EC9-32D5F69181C0'Parameters
Section titled “Parameters”Query Parameters
Section titled “Query Parameters”Active NavTab: all / active / errors / scheduled (default all).
Primary-trigger kind filter (manual/schedule/git/webhook/…).
Status-bucket filter (running/done/error/cancelled/ready).
Free-text search over the workflow name.
Whitelisted sort column: name | created_at | last_run_at.
Default created_at.
asc | desc (default desc).
Recompute the NavTab facet counts (default true). The board sends false
for a tab/page change (the counts depend only on the project).
Responses
Section titled “Responses”GET /api/workflows/board body — one page of a project’s workflows + total +
the NavTab facet counts.
object
A workflow without its steps — used for list responses.
#358 list board: the summary is enriched with the per-row badges the board
renders — node_count (“N nodes”), the last run’s last_run_status /
last_run_at (the STATUS badge + relative time) and the workflow’s
primary_trigger_kind (the trigger glyph + label). All four are computed in
the list query (list_workflow_summaries_by_project),
not on the workflows row, so the From<&models::Workflow> projection is
gone — the handler maps the enriched row directly.
Serde casing: snake_case (no rename_all; field names already snake_case),
matching every other field on this struct and the existing zod schema.
object
#358: when the most recent run started (falling back to its created_at
when it never started), or None when the workflow has never run.
#358: status of the most recent run of this workflow (completed /
failed / running / …), or None when the workflow has never run.
Issue #110: per-workflow concurrency cap; None = unlimited.
#358: number of workflow_nodes in this workflow’s graph.
#358: the kind of the workflow’s primary trigger (manual / schedule
/ git / …) for the row’s trigger glyph + label, or None when the
workflow has no trigger configured.
#626 (S1 contract-request) — number of runs of this workflow in the last
24h (any status). Drives the board’s runs column. The per-row analog of
the project-aggregate WorkflowStats.
#626 (S1 contract-request) — success rate (0..1) over the last 24h —
completed / (completed + failed); None (rendered as a dash) when no
terminal run in the window. Drives the board’s success-rate column.
#358: a CONCRETE one-line detail of the primary trigger for the row
sub-line (git → branch/ref, telegram → command, email-plus → +slug,
email → subject filter). None when the trigger has no useful
concrete detail (manual / webhook / schedule / discord / imap) — the row then
shows the project alone. Distinct from the kind glyph above; never a
generic label (those would need i18n, so they stay None).
Examplegenerated
{ "facets": { "active": 1, "all": 1, "errors": 1, "scheduled": 1 }, "items": [ { "created_at": "2026-04-15T12:00:00Z", "id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "last_run_at": "2026-04-15T12:00:00Z", "last_run_status": "example", "max_concurrent_runs": 1, "name": "example", "node_count": 1, "primary_trigger_kind": "example", "project_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "runs_24h": 1, "success_rate_24h": 1, "trigger_hint": "example" } ], "total": 1}Structured client error
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Authentication required
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Permission denied
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}Structured server error
The canonical JSON body of every error response — the single source of truth
the frontend binds to. Every AppError serializes as this exact shape, and
the generated OpenAPI component ApiErrorBody (with its ErrorCode enum) is
what the frontend error schema is generated from, so there is no hand-written
error schema on either end.
object
Machine-readable, stable error code.
Present only on a quota-exceeded 403 — the inline upgrade-CTA payload.
object
The entitlement feature key that was hit, e.g. apps.max_count.
The plan’s limit for this key.
Where to send the user to upgrade.
Current usage (count or bytes, per the key).
Human-readable message (the server’s English text; the client may localize
by code).
Example
{ "code": "not_found"}