Skip to content
Select themeSelect language

`GET /api/workflows/board?project_id=<uuid>` — one server-paginated, tab/ trigger/status-filtered page of a project's workflows + total + NavTab facets.

GET
/api/workflows/board
curl --request GET \
--url 'https://example.com/api/workflows/board?project_id=2489E9AD-2EE2-8E00-8EC9-32D5F69181C0'
project_id
required
string format: uuid
tab
string

Active NavTab: all / active / errors / scheduled (default all).

trigger
string

Primary-trigger kind filter (manual/schedule/git/webhook/…).

status
string

Status-bucket filter (running/done/error/cancelled/ready).

q
string

Free-text search over the workflow name.

sort
string

Whitelisted sort column: name | created_at | last_run_at. Default created_at.

dir
string

asc | desc (default desc).

limit
integer format: int64
offset
integer format: int64
include_facets
boolean

Recompute the NavTab facet counts (default true). The board sends false for a tab/page change (the counts depend only on the project).

Media typeapplication/json

GET /api/workflows/board body — one page of a project’s workflows + total + the NavTab facet counts.

object
facets
One of:
null
items
required
Array<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
created_at
required
string format: date-time
id
required
string format: uuid
last_run_at

#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.

string | null format: date-time
last_run_status

#358: status of the most recent run of this workflow (completed / failed / running / …), or None when the workflow has never run.

string | null
max_concurrent_runs

Issue #110: per-workflow concurrency cap; None = unlimited.

integer | null format: int32
name
required
string
node_count
required

#358: number of workflow_nodes in this workflow’s graph.

integer format: int64
primary_trigger_kind

#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.

string | null
project_id
required
string format: uuid
runs_24h
required

#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.

integer format: int64
success_rate_24h

#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.

number | null format: double
trigger_hint

#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).

string | null
total
required
integer format: int64
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

Media typeapplication/json

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
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}

Authentication required

Media typeapplication/json

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
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}

Permission denied

Media typeapplication/json

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
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}

Structured server error

Media typeapplication/json

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
code
required

Machine-readable, stable error code.

string
Allowed values: not_found unauthorized forbidden bad_request unprocessable precondition_failed conflict method_not_allowed rate_limited quota_exceeded database_error docker_error vault_error internal_error
details
One of:
null
error
required

Human-readable message (the server’s English text; the client may localize by code).

string
Example
{
"code": "not_found"
}