Workflow API
Table of Contents
upsquad/workflow/v1/workflow.proto
AnswerRunPromptRequest
AnswerRunPromptRequest records an answer to a mid-run user_input prompt. LBE-B2 (#2249).
| Field | Type | Label | Description |
|---|---|---|---|
| prompt_id | string | prompt_id is the governance_approvals row id to answer (required). | |
| choice | string | choice is the selected option value. Required unless the prompt is free-text-only (allow_text with no options); when set it must be one of the prompt's option values. | |
| text | string | text is the free-text answer. Permitted only when the prompt has allow_text=true; rejected otherwise. |
AnswerRunPromptResponse
AnswerRunPromptResponse reports the outcome of an answer attempt. LBE-B2 (#2249).
| Field | Type | Label | Description |
|---|---|---|---|
| recorded | bool | recorded is true when THIS call won the first-write and recorded the answer. False when the prompt was already answered before this call (already_answered is then true) — the answer the client should render is the recorded one. | |
| already_answered | bool | already_answered is true when the prompt was already resolved (this call was a benign no-op — first-write-wins). recorded is then false. | |
| prompt | GetRunPromptResponse | prompt is the prompt's post-answer projection (status flips to "answered" on a successful record), so the client re-renders the card from a single response. |
ApproveActionRequest
ApproveActionRequest approves a workflow action.
| Field | Type | Label | Description |
|---|---|---|---|
| action_id | string | action_id is the workflow action UUID (required). | |
| comment | string | comment is optional approval comment. |
ApproveActionResponse
ApproveActionResponse contains the approved action result.
| Field | Type | Label | Description |
|---|---|---|---|
| action | WorkflowAction | action is the updated workflow action. | |
| next_actions | WorkflowAction | repeated | next_actions are any new actions created as a result. |
CancelRunRequest
CancelRunRequest cancels a workflow run.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the workflow run UUID to cancel (required). | |
| reason | string | reason is an optional human-readable note recorded in the audit log. |
CancelRunResponse
CancelRunResponse returns the post-cancel run state.
| Field | Type | Label | Description |
|---|---|---|---|
| run | WorkflowRun | run is the workflow run with status='cancelled' (or unchanged if the run was already terminal - idempotent). |
CreateWorkflowRequest
CreateWorkflowRequest creates a new workflow.
| Field | Type | Label | Description |
|---|---|---|---|
| name | string | name is the workflow name (required). | |
| description | string | description is an optional workflow description. | |
| definition | google.protobuf.Struct | definition is the workflow definition JSON (required, validated against schema). | |
| team_id | string | team_id is optional team scoping. | |
| visibility | string | visibility is the workflow's audience: "team" (default when empty — visible to its team, today's universal behaviour) |
CreateWorkflowResponse
CreateWorkflowResponse contains the created workflow.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow | Workflow | workflow is the created workflow. |
DefinitionValidationError
DefinitionValidationError is attached as a google.rpc.Status detail on the InvalidArgument error CreateWorkflow and UpdateWorkflowDefinition return when a workflow definition fails JSON-schema validation (core#2530, split from #2515). It carries the INNERMOST (leaf) schema failures — never the root/composition node — so the visual builder (client#749 Bld-4) can attach each failure to the exact node/input it belongs to via the JSON pointer, instead of parsing the prose status message. This ADDS renderable field-level detail; the status message itself stays human-readable and the semantic-validation messages are unchanged.
| Field | Type | Label | Description |
|---|---|---|---|
| violations | DefinitionValidationError.FieldViolation | repeated | Innermost leaf-level schema violations, in tree-traversal order. Always carries at least one entry when present. |
DefinitionValidationError.FieldViolation
FieldViolation is one {json-pointer, message} pair identifying a single schema failure.
| Field | Type | Label | Description |
|---|---|---|---|
| instance_location | string | RFC 6901 JSON pointer into the submitted definition, addressing the innermost offending value — e.g. "/steps/loop1/max_iterations" or "/steps/review/required_clearance". The empty string denotes the document root (a top-level failure). | |
| message | string | Human-readable schema violation text taken verbatim from the jsonschema leaf error — e.g. "must be <= 25" or "value must be one of "agent_action", "approval_gate", ...". |
DeleteWorkflowRequest
DeleteWorkflowRequest soft-deletes a workflow. #2002.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow_id | string | workflow_id is the workflow UUID (required). |
DeleteWorkflowResponse
DeleteWorkflowResponse acknowledges a soft-delete. Empty by design: the workflow is gone from List/Get after this call, so there is nothing useful to echo back. A typed response (rather than google.protobuf.Empty) leaves room to add a deleted_at timestamp or a runs-hidden count later without a breaking change. #2002.
DeliverWorkflowEventRequest
DeliverWorkflowEventRequest delivers one external event to a run parked on a
signal-mode wait node. WR2-4 Slice 6 (#2266), LLD #2253 §3.4.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the workflow run UUID to deliver into (required). The run is read RLS-scoped: a run owned by another org is NOT_FOUND. | |
| event_key | string | event_key is the correlation handle the waiting node declared (dsl.Step.event_key). It must match for the wait to resolve; a delivery on a key nothing is waiting on is buffered in the run's state and consumed if a wait on that key is reached later. Required. | |
| payload | bytes | payload is the OPAQUE event body as JSON bytes. bytes rather than google.protobuf.Struct on purpose: an external event body (CI metadata, a PR number, a vendor webhook) must ride through without a proto-struct constraint. The interpreter never inspects it — it binds it verbatim as the wait node's output envelope, and the SUCCESSOR's WR2-1 expects FieldSpec is what structurally validates the shape. |
Optional: an absent payload (and an explicit JSON null) both normalise to {} at the interpreter's drain, so a bare "it happened" event resolves the wait and still binds a valid empty object. |
| delivery_id | string | | delivery_id is the PRODUCER-supplied identity of THIS delivery attempt (required). It is what makes an at-least-once caller safe: the platform burns (org, run_id, event_key, delivery_id) durably before signalling, so re-sending the SAME delivery_id is a no-op even after the waiting node already consumed the first one. A genuinely NEW occurrence must carry a NEW delivery_id — a retry and a second occurrence are indistinguishable without it, which is exactly why the field is required rather than optional.
Opaque to the platform: whatever stable id the caller's retry loop re-sends (a webhook delivery id, an outbox row id, a broker message id). Empty is INVALID_ARGUMENT — never a silent fall-through to the non-idempotent path. |
DeliverWorkflowEventResponse
DeliverWorkflowEventResponse reports the outcome of a delivery. WR2-4 Slice 6 (#2266).
| Field | Type | Label | Description |
|---|---|---|---|
| run | WorkflowRun | run is the run's CURRENT projection. Delivery is asynchronous — the run's status transitions when the interpreter observes the signal and re-projects (the projection is the single writer of a Temporal run's status) — so this is the state at delivery time, not a post-resume state. | |
| deduplicated | bool | deduplicated is true when this delivery_id had ALREADY been accepted for (run_id, event_key): the call was a benign no-op and NO signal was sent. An at-least-once caller can treat true exactly like a success. |
EscalateActionRequest
EscalateActionRequest escalates a workflow action.
| Field | Type | Label | Description |
|---|---|---|---|
| action_id | string | action_id is the workflow action UUID (required). | |
| comment | string | comment is optional escalation comment. |
EscalateActionResponse
EscalateActionResponse contains the escalated action result.
| Field | Type | Label | Description |
|---|---|---|---|
| action | WorkflowAction | action is the updated workflow action. |
GetNodeIORequest
GetNodeIORequest retrieves the input/output payloads of one workflow_action.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the parent run UUID (required, used for tenant scoping). | |
| node_id | string | node_id is the workflow_action UUID (required). |
GetNodeIOResponse
GetNodeIOResponse returns the redacted input + output of a node.
| Field | Type | Label | Description |
|---|---|---|---|
| input | google.protobuf.Struct | input is the action's input JSON, with sensitive top-level keys stripped per the caller's clearance. | |
| output | google.protobuf.Struct | output is the action's output JSON, with sensitive top-level keys stripped per the caller's clearance. Empty if the node has not produced output yet (queued / running / blocked). | |
| redacted_fields | string | repeated | redacted_fields lists every top-level key removed from input or output. Empty when no redaction occurred (caller has full clearance). |
GetNodeTranscriptRequest
GetNodeTranscriptRequest retrieves the agent turn transcript of one node.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the parent run UUID (required, used for run + tenant scoping). | |
| node_id | string | node_id identifies the node whose transcript is requested (required). It accepts EITHER form (#2021): - the workflow-DAG node id, i.e. the step_name ("architect", "fetch_issue") — this is what the run-view client sends; OR - the workflow_action UUID (back-compat). Both map to the node's step_name, which keys the captured session_events. An unknown or foreign node_id yields an empty transcript (session_missing), never an error. |
GetNodeTranscriptResponse
GetNodeTranscriptResponse returns the ordered, redacted transcript events of a node across ALL of its attempts.
| Field | Type | Label | Description |
|---|---|---|---|
| events | TranscriptEvent | repeated | events are ordered by (session_id, seq, created_at). Multiple attempts interleave by session_id — each event carries its own session_id. |
| redacted_fields | string | repeated | redacted_fields lists every content field removed across all events (stable, deduplicated). Empty when no redaction occurred (caller has full clearance). |
| session_missing | bool | session_missing is true when the node has no captured transcript — a historical / non-agent node, or one whose capture was lost (best-effort). The client renders an empty state rather than an error. |
GetRunPromptRequest
GetRunPromptRequest fetches one mid-run user_input prompt's detail. LBE-B2 (#2249).
| Field | Type | Label | Description |
|---|---|---|---|
| prompt_id | string | prompt_id is the governance_approvals row id the run parked on — the node's blocked_by / approval_id uuid (required). RLS-scoped to the caller's org. |
GetRunPromptResponse
GetRunPromptResponse is the projected spec of a mid-run HUMAN-ANSWERABLE gate, plus the caller-specific can_answer verdict. LBE-B2 (#2249); widened to serve WR2-5 question gates by core#2514 CORE-B1.
TWO KINDS ride this one message, discriminated by the SERVED kind field:
"user_prompt" — the LBE-B1 prompt. question / options[].value / allow_text /
text_placeholder / default_choice carry it; the answer goes to
WorkflowService.AnswerRunPrompt as {choice, text}.
"question_gate" — the WR2-5 structured question. `question` carries
question_spec.prompt; `options[].value` carries each option's
KEY (the routing token, isomorphic to a prompt choice, so an
options-first card needs no new model); `answer_fields` carries
the typed answer schema; `default_choice` carries
default_option_key. allow_text is ALWAYS false — a question has
no free-text slot, it has typed answer fields. The answer goes
to ApprovalService.RecordDecision as an APPROVED decision whose
response_payload is {option_key, fields}, and that RPC
pre-flights it against this same spec (core#2514 B4).
A single RPC serving kind is deliberate: the discriminator is exactly what a
client lacks (a Quad dock item or an Approvals row is just an approval uuid), so
a sibling GetRunQuestion would require the caller to already know the answer to
the question the RPC exists to answer, and probe-and-fallback across two RPCs is
a race.
| Field | Type | Label | Description |
|---|---|---|---|
| prompt_id | string | prompt_id echoes the requested row id. | |
| question | string | question is the human-facing prompt headline (from prompt_spec.question). | |
| options | RunPromptOption | repeated | options are the structured choices the operator may pick from. Empty for a free-text-only prompt. On a question_gate each option's value is the question_spec option KEY — the token the answer's option_key must equal and the one the run routes N-way on. The option's next step is deliberately NOT served: routing is resolved server-side and a client must never predict it. |
| allow_text | bool | allow_text permits a free-text answer in addition to (or instead of) a choice. ALWAYS false on a question_gate: a question collects TYPED fields (answer_fields), not one free-text blob, and reporting true would make a card render a text box whose value the question write path discards. | |
| text_placeholder | string | text_placeholder is the display hint for the free-text field (display-only). | |
| default_choice | string | default_choice is the option value recorded on an on_timeout="default" expiry (surfaced so the UI can pre-select it). Empty when the prompt has no default. On a question_gate this is question_spec.default_option_key. | |
| required_clearance | int32 | required_clearance is the clearance floor a caller must clear to answer (0 when the prompt carries no floor — anyone with view access may answer). | |
| expires_at | google.protobuf.Timestamp | expires_at is when the prompt window closes and the on_timeout policy fires — i.e. the deadline the run's own timer will honour. |
core#2514 B5: this is now derived from the parked node's declared timeout_minutes (requested_at + N), which is what the interpreter arms its Temporal timer with. It used to be the governance row's expires_at, which is a flat 24h on every Temporal-opened gate — so a timeout_minutes: 60 question told its answerer they had a day (measured). This is a repair to the contract this comment ALREADY stated, not a change to it. A node that declares no window (and every row opened before #2514) still reports the row expiry, unchanged. The row's own expiry is served separately as governance_expires_at.
⚠ THE MEANING VARIES BY ROW VINTAGE AND THE WIRE CARRIES NO MARKER. A row opened before #2514 falls back to the 24h governance expiry, and so does a node that declared no window — both report expires_at == governance_expires_at, and nothing distinguishes "no declared window" from "opened before the stamp existed". A client that wants to render "deadline unknown" rather than "24h" therefore has no signal today; treat equality of the two fields as "unconstrained window", not as a precise 24h promise. | | status | string | | status is the prompt lifecycle: "pending" (awaiting an answer), "answered" (resolved with an answer), or "closed" (denied / expired without an answer). | | can_answer | bool | | can_answer reports whether THIS caller may answer the prompt right now, computed server-side from the required_clearance floor AND the answerable_by scope against the authenticated identity. False on an already-resolved prompt. | | answered_by | string | | answered_by is the member id (or timeout sentinel) that recorded the answer, set only when status="answered". Empty otherwise. | | answered_choice | string | | answered_choice is the recorded answer's choice value when status="answered" (so a re-opened card renders "answered: <choice>"). Empty on a pending prompt. On a question_gate this is the answered option_key — the same machine token, so the same card line renders both kinds. | | answered_text | string | | answered_text is the recorded answer's free text when status="answered". Empty on a pending prompt or a choice-only answer. | | asked_by_step | string | | asked_by_step is the agent_action step whose agent posed this question (LBE-B3, #2292): either the DSL-declared asked_by or the last-completed agent step on the run's executed path. Empty on a pre-B3 prompt or a prompt reached before any agent turn. Recovered from the prompt row metadata (asked_by_step). | | asked_by_role | string | | asked_by_role is the asking step's agent_role — the human-legible identity of the agent asking (e.g. "eng"). Recovered from row metadata (asked_by_role). | | asked_by_agent_id | string | | asked_by_agent_id is the CONCRETE deployed-agent uuid the asking step resolved its role to (workflow_actions.resolved_agent_id), so the card can deep-link to the exact agent. Empty when the step never ran a turn or on a legacy row. | | kind | string | | kind is the SERVED answering-surface discriminator: "user_prompt" or "question_gate" (byte-identical to governance_approvals.action_type). It tells the client which write path to use and which of the fields above/below are meaningful. NEVER infer it — from asked_by_role, from the presence of answer_fields, or from a probe: the same row is reachable from the DAG, the Quad dock and the Approvals list, and an inferred discriminator is a race. | | answer_fields | RunPromptAnswerField | repeated | answer_fields is the question's TYPED answer schema (question_spec.answer), ordered by field name — a server-defined, stable order, because the underlying declaration is a map and rendering a map range would reorder the form between reloads. Empty on a user_prompt and on an options-only question. | | answerable_by | string | | answerable_by is the served answer scope: "" or "anyone" (any viewer in the org may answer) or "requester" (only the run's originating human). can_answer is the computed verdict for THIS caller; this field is the REASON, so a card can say "only the person who started this run can answer" instead of greying out silently. | | governance_expires_at | google.protobuf.Timestamp | | governance_expires_at is the governance_approvals row's OWN expiry — the Approvals-queue backstop the deadline sweeper acts on, a flat 24h on every Temporal-opened gate. It is NOT the run's window (expires_at is), and the two genuinely differ: after expires_at passes, the run has already applied its on_timeout policy while the row can remain in the queue until this instant. Served so that fact is on the wire rather than a surprise. | | answered_fields | string | | answered_fields is the recorded typed answer object of an answered question_gate, as a raw JSON object string (e.g. {"risk_score":7}). Raw rather than a Struct so the operator's numbers are echoed byte-for-byte, never round -tripped through a double. Empty on a user_prompt, a pending row, or an answer that carried no fields. |
GetToolCatalogRequest
GetToolCatalogRequest requests the tool_action whitelist. It carries no parameters: the whitelist is not tenant-scoped. It IS deployment-scoped — see GetToolCatalogResponse.tools. #2067, #2566.
GetToolCatalogResponse
GetToolCatalogResponse returns the whitelisted tools a tool_action step may invoke, each with its whitelisted actions and their argument names, PLUS the admitted poll-mode wait observation registry. #2067, #2527.
| Field | Type | Label | Description |
|---|---|---|---|
| tools | ToolCatalogEntry | repeated | tools is the set of governed tools this deployment can actually RUN: the WF-30 create-time whitelist restricted to the tools whose provider the runtime has mounted. A tool behind a process-level master gate that is off (today: code_exec, gated by WF_CODE_EXEC_ENABLED) is OMITTED rather than advertised, because a node built from it would validate at create, render on the canvas, and then fail closed at dispatch — #2566. Membership is therefore a serve-ability statement, not a statement about what the server binary was compiled with, and it can differ between environments. A client MUST render its tool picker from this list and MUST treat a tool_action node in an existing definition whose tool is absent here as "not available in this environment" rather than as an invalid step. |
| poll_observations | PollObservation | repeated | poll_observations is the admitted poll-mode wait observation registry: the (tool, action) pairs a poll-mode wait step may name. It is served from the SAME table the engine admits from (observation_registry.go's ObservationSafeActions — the fail-closed allowlist validateWait's poll arm enforces), so the builder's poll-target picker (PRD #2081 §4.7 FR 7.9) can never drift from what the engine accepts: a client MUST render the picker from this field, never a hard-coded copy of the names. This is a SUPPORTED-LIST, not an integration surface — poll admission stays deliberately narrow, so the surface is intentionally the existing four, not a widened set. The narrowness is a product decision about blast radius, not a consequence of the lap-blind idempotency key that used to fence it: that defect is closed (#2397), and the surface grows only when a row is added to ObservationSafeActions on its own security review. #2527. |
GetWorkflowRequest
GetWorkflowRequest retrieves a workflow by ID.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow_id | string | workflow_id is the workflow UUID (required). |
GetWorkflowResponse
GetWorkflowResponse contains the requested workflow.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow | Workflow | workflow is the requested workflow. |
GetWorkflowStatusRequest
GetWorkflowStatusRequest retrieves workflow run status.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the workflow run UUID (required). |
GetWorkflowStatusResponse
GetWorkflowStatusResponse contains workflow run status and actions.
| Field | Type | Label | Description |
|---|---|---|---|
| run | WorkflowRun | run is the workflow run with status. | |
| actions | WorkflowAction | repeated | actions are the workflow actions for this run. |
ListPendingApprovalsRequest
ListPendingApprovalsRequest lists actions awaiting approval.
| Field | Type | Label | Description |
|---|---|---|---|
| agent_id | string | agent_id optionally filters by assigned agent. | |
| limit | int32 | limit controls pagination (default: 50, max: 100). | |
| offset | int32 | offset controls pagination. |
ListPendingApprovalsResponse
ListPendingApprovalsResponse contains pending approvals with context.
| Field | Type | Label | Description |
|---|---|---|---|
| approvals | PendingApproval | repeated | approvals are the pending approval entries. |
| total_count | int32 | total_count is the total number of pending approvals (for pagination). |
ListRunsRequest
ListRunsRequest pages through past runs of one workflow.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow_id | string | workflow_id is the workflow whose runs to list (required). | |
| page_size | int32 | page_size caps the rows in the response. Default 20, max 100. Values outside (0, 100] are clamped silently. | |
| page_token | string | page_token is the opaque cursor returned by a previous ListRuns call's next_page_token. Empty starts from the newest run. | |
| status_filter | RunStatusFilter | status_filter narrows the result set; see RunStatusFilter. |
ListRunsResponse
ListRunsResponse returns one page of WorkflowRun rows.
| Field | Type | Label | Description |
|---|---|---|---|
| runs | WorkflowRun | repeated | runs is the page of WorkflowRun rows ordered (started_at DESC, id DESC). nodes_total / nodes_done are populated; cost_usd reflects the engine's rolled-up cost (zero until #1127 lands the cost-meter wiring). |
| next_page_token | string | next_page_token is the cursor to pass to the next ListRuns call. Empty means no more pages. |
ListVisibleRunsRequest
ListVisibleRunsRequest lists the caller's visible runs (QWP-3, #2359). Unlike ListRunsRequest there is NO workflow_id: the visibility predicate (team membership OR private ownership, both keyed off app.member_id) is the scoping.
| Field | Type | Label | Description |
|---|---|---|---|
| page_size | int32 | page_size caps the rows in the response. Default 20, max 100. Values outside (0, 100] are clamped silently. | |
| page_token | string | page_token is the opaque cursor returned by a previous ListVisibleRuns call's next_page_token. Empty starts from the newest run. Same keyset shape as ListRuns (started_at DESC, id DESC). | |
| status_filter | RunStatusFilter | status_filter narrows the result set; see RunStatusFilter. The panel defaults to RUN_STATUS_FILTER_ACTIVE (pending |
ListVisibleRunsResponse
ListVisibleRunsResponse is a page of VisibleRunRow ordered (started_at DESC, id DESC), plus the cursor for the next page.
| Field | Type | Label | Description |
|---|---|---|---|
| runs | VisibleRunRow | repeated | runs is the page of VisibleRunRow rows the caller may see. |
| next_page_token | string | next_page_token is the cursor to pass to the next ListVisibleRuns call. Empty means no more pages. |
ListWorkflowsRequest
ListWorkflowsRequest lists workflows with optional filtering.
| Field | Type | Label | Description |
|---|---|---|---|
| team_id | string | team_id optionally filters by team. | |
| status | string | status optionally filters by workflow status. | |
| limit | int32 | limit controls pagination (default: 50, max: 100). | |
| offset | int32 | offset controls pagination. |
ListWorkflowsResponse
ListWorkflowsResponse contains the workflow list.
| Field | Type | Label | Description |
|---|---|---|---|
| workflows | Workflow | repeated | workflows is the list of workflows. |
| total_count | int32 | total_count is the total number of workflows (for pagination). |
NodeIOFrame
NodeIOFrame is one live LLM-I/O frame for an agent node, mirroring the wfio wire contract the orchestrator publisher tees (LBE-A1). Exactly one event arm is set.
| Field | Type | Label | Description |
|---|---|---|---|
| node_id | string | node_id is the step name the frame belongs to. | |
| session_id | string | session_id is the attempt id (groups redos/laps; mirrors TranscriptEvent). | |
| seq | int64 | seq is the publisher's monotonic sequence within (run, node, session). A jump in seq means the publisher dropped frames — the client should expect a gap. | |
| iteration | int32 | iteration is the loop lap (mirrors NodeStatusUpdate.iteration); 0 outside a loop. | |
| token_text | string | token_text is a coalesced, security-filtered batch of LLM tokens. Suppressed (unset) for callers below clearance 4 (structural-frames-only floor). | |
| tool_call | NodeToolCall | tool_call is a tool invocation surfaced live. | |
| tool_result | NodeToolResult | tool_result is a completed tool result surfaced live. | |
| status | string | status is an agent status transition (thinking | |
| user_prompt | UserPromptFrame | user_prompt is a mid-run elicitation prompt (Capability B / LBE-B). Structural, delivered at every clearance so the "answer needed" card can render. |
NodeStatusUpdate
NodeStatusUpdate is a single transition or counter update for one node (workflow_action) inside the subscribed run. The frontend live-DAG keys updates by (run_id, node_id) and replaces the previous projection.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the parent run UUID. | |
| node_id | string | node_id is the workflow_action UUID. | |
| status | string | status is one of: queued | |
| elapsed_ms | int64 | elapsed_ms is wall-clock time spent in running so far for this node. For terminal nodes this is the final duration; for blocked nodes this is the time since the gate was hit. | |
| cost_usd | double | cost_usd is the running cost rolled up to this node (sum of LLM / tool costs charged to this action). Convention matches analytics.proto: USD as double. | |
| blocked_by | string | blocked_by says WHY this node is parked when status=blocked. Empty otherwise. It carries one of two things and a client MUST branch before using it: |
a SUB-KIND DISCRIMINATOR — a fixed token naming the kind of park, currently only "awaiting-question": the node is a WR2-5 question waiting on a structured human answer (#2263). The public status palette is fixed at five colours, so such a node arrives as blocked exactly like an approval gate and this token is the ONLY thing distinguishing them. The governance row's uuid is still available as WorkflowAction.approval_id (ListWorkflowActions).
otherwise the APPROVAL UUID this node is waiting on (or a legacy free-text "Awaiting Dana W." label), which the frontend deep-links to ApprovalService.GetApproval.
A visually distinct colour for the awaiting-human sub-kind is a frontend follow-up; the wire palette stays at five values. | | at | google.protobuf.Timestamp | | at is the timestamp of the underlying transition. | | iteration | int32 | | iteration is the loop-lap ordinal for this node (WR2-2, migration 176): 0 for every non-loop step, and the 1-based lap number for a step executed inside a bounded loop's body. The live-DAG uses it to group a loop body step's repeated executions into ordered "round 1 / round 2 / ..." rows instead of collapsing them onto a single node. Additive and back-compatible: an existing consumer that ignores the field sees today's behaviour (a no-loop run streams iteration 0 everywhere). Client rendering of rounds is a separate upsquad-client task. |
NodeToolCall
NodeToolCall is the live tool-invocation payload of a NodeIOFrame.
| Field | Type | Label | Description |
|---|---|---|---|
| tool_name | string | tool_name is the invoked tool. | |
| tool_call_id | string | tool_call_id correlates the call with its later NodeToolResult. | |
| args_summary | string | args_summary is a compact, clamped, redaction-safe rendering of the arguments (structural chrome; the durable + un-clamped truth is the transcript). |
NodeToolResult
NodeToolResult is the live tool-result payload of a NodeIOFrame.
| Field | Type | Label | Description |
|---|---|---|---|
| tool_call_id | string | tool_call_id correlates the result with its NodeToolCall. | |
| tool_name | string | tool_name is the tool that produced the result. | |
| summary | string | summary is a compact, clamped rendering of the result. Suppressed (empty) for callers below clearance 4 — the same free-text floor as token_text. | |
| is_error | bool | is_error is true when the tool returned an error. | |
| duration_ms | int32 | duration_ms is the tool call's wall-clock duration. |
PauseRunRequest
PauseRunRequest pauses a workflow run at the next checkpoint.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the workflow run UUID to pause (required). | |
| reason | string | reason is an optional human-readable note recorded in the audit log. |
PauseRunResponse
PauseRunResponse returns the post-pause run state.
| Field | Type | Label | Description |
|---|---|---|---|
| run | WorkflowRun | run is the workflow run with status='paused' (or unchanged if the run was already paused / terminal - idempotent). |
PendingApproval
PendingApproval represents an approval awaiting decision with context.
| Field | Type | Label | Description |
|---|---|---|---|
| action | WorkflowAction | action is the workflow action awaiting approval. | |
| workflow_name | string | workflow_name is the parent workflow name. | |
| agent_name | string | agent_name is the assigned agent name (if available). |
PollObservation
PollObservation is one admitted poll-mode wait observation (tool, action)
pair from the observation-safe registry. Unlike a ToolCatalogEntry it carries
no argument schema of its own: the amendment's §3.3.1 "no second construction
path" decision routes a poll observation's with rules through the SAME
ToolCatalogEntry/ToolCatalogAction a tool_action uses when one exists (the
github.* observations are also catalogued for exactly this reason), never a
second copy that could drift. Membership here mirrors the engine's fail-closed
admission set exactly. #2527.
| Field | Type | Label | Description |
|---|---|---|---|
| tool | string | tool is the observation provider namespace (e.g. "platform", "github"). | |
| action | string | action is the observation-safe read (e.g. "read_run_state", "get_check_runs"). | |
| rationale | string | rationale is the review-gated read-only/cheap/idempotent justification for admitting this observation — human-readable context the picker may surface. |
RejectActionRequest
RejectActionRequest rejects a workflow action.
| Field | Type | Label | Description |
|---|---|---|---|
| action_id | string | action_id is the workflow action UUID (required). | |
| comment | string | comment is optional rejection comment. |
RejectActionResponse
RejectActionResponse contains the rejected action result.
| Field | Type | Label | Description |
|---|---|---|---|
| action | WorkflowAction | action is the updated workflow action. |
ResumeRunRequest
ResumeRunRequest lifts a pause on a workflow run. WF-16 (#1819).
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the workflow run UUID to resume (required). | |
| reason | string | reason is an optional human-readable note recorded in the audit log. |
ResumeRunResponse
ResumeRunResponse returns the run state after the resume signal is sent. WF-16 (#1819).
| Field | Type | Label | Description |
|---|---|---|---|
| run | WorkflowRun | run is the workflow run. Resume is honored asynchronously by the interpreter at the next step boundary, so the returned status reflects the run's current projection (e.g. still 'paused' until the interpreter observes the signal and re-projects 'running'). |
RunLogLine
RunLogLine is a single log entry emitted during workflow execution.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the parent run UUID. | |
| node_id | string | node_id is the workflow_action UUID the log line belongs to. Empty for run-level events (run started, run cancelled, run paused). | |
| level | string | level is one of INFO | |
| message | string | message is the human-readable log line. | |
| t | google.protobuf.Timestamp | t is the timestamp of the log emission. |
RunPromptAnswerField
RunPromptAnswerField is one declared field of a question gate's typed answer schema (question_spec.answer[name] → dsl.FieldSpec). core#2514 CORE-B1.
It is the ONE genuinely new shape a question adds over a prompt: a prompt has a single free-text slot, a question has N typed fields. The answer the client sends must satisfy these declarations — ApprovalService.RecordDecision runs exactly the same dsl.CheckShape over exactly this schema BEFORE it burns the approval's first-write CAS, and returns the violations verbatim.
| Field | Type | Label | Description |
|---|---|---|---|
| name | string | name is the answer object's key (required) — what the client must use in the response_payload's fields object. | |
| type | string | type is the declared field type: one of string | |
| required | bool | required, when true, means the answer object MUST carry this field. A missing required field is rejected before the CAS with a renderable violation. | |
| description | string | description is the author's optional help text for the field (display-only). |
RunPromptOption
RunPromptOption is one structured choice in a user_input prompt. value is the
machine token that lands in the answer's choice; label/description are
display-only. Mirrors the DSL PromptOption (LBE-B1). LBE-B2 (#2249).
It also carries a WR2-5 question's options, where value is the option KEY (the
token the answer's option_key must equal and the routing selector) and label is
the display text — the two shapes are isomorphic, which is why a question needs no
second option model on the client. The option's next step is not exposed:
routing is a server-side resolution and a client must never predict it.
| Field | Type | Label | Description |
|---|---|---|---|
| value | string | value is the machine token recorded as the answer's choice (required). | |
| label | string | label is the display text for the option (optional). | |
| description | string | description is a longer display hint for the option (optional). Always empty for a question option (dsl.QuestionOption has no description). |
SetWorkflowEnabledRequest
SetWorkflowEnabledRequest configures a workflow's trigger + the enabled gate. WF-24 (#1888), the W4 schedule pre-wiring.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow_id | string | workflow_id is the workflow UUID (required). | |
| enabled | bool | enabled is the schedule-firing gate (W4). false leaves the workflow manual-only / dormant. | |
| trigger_type | string | trigger_type is "manual" (default when empty) or "schedule". | |
| trigger_config | google.protobuf.Struct | trigger_config is the schedule cron/interval spec (unset for manual). |
SetWorkflowEnabledResponse
SetWorkflowEnabledResponse returns the workflow with its new trigger config and enabled state.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow | Workflow | workflow is the updated workflow. |
StreamGap
StreamGap signals that one or more NodeIOFrames were dropped between the last delivered seq and the next one, so the client can render a "stream resynced" marker and optionally refetch the snapshot. from_seq is the last seq the client saw before the gap; to_seq is the next seq it will see.
| Field | Type | Label | Description |
|---|---|---|---|
| node_id | string | node_id is the step whose frames were dropped (empty when not node-specific). | |
| from_seq | int64 | from_seq is the last seq delivered before the gap (0 when unknown). | |
| to_seq | int64 | to_seq is the first seq that will be delivered after the gap. |
StreamNodeIORequest
StreamNodeIORequest subscribes to a run's live agent-step LLM I/O.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the workflow run UUID to stream (required). | |
| node_id | string | node_id optionally scopes the stream to a single agent node (step name). Empty means every agent node in the run. The snapshot honours this filter too. |
StreamNodeIOResponse
StreamNodeIOResponse is one frame of the node-I/O stream. Exactly one of the oneof arms is set on any given message; snapshot_done marks the transition from the replayed transcript snapshot to live frames.
| Field | Type | Label | Description |
|---|---|---|---|
| snapshot_event | TranscriptEvent | snapshot_event replays one already-captured transcript event (the verbatim GetNodeTranscript shape, clearance-redacted). Sent before snapshot_done. | |
| live | NodeIOFrame | live is a live frame coalesced from the running agent step (after snapshot_done). token_text is present only for clearance >= 4. | |
| gap | StreamGap | gap signals that frames were dropped (either upstream at the publisher — a jump in seq — or in this hub for a slow subscriber). The client may refetch the snapshot to resync. | |
| snapshot_done | bool | snapshot_done is true on the single marker frame that ends the snapshot replay and precedes the first live frame. All other fields are unset on the marker. |
StreamRunLogsRequest
StreamRunLogsRequest subscribes to live log lines for one run.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the workflow run UUID to subscribe to (required). |
StreamRunLogsResponse
StreamRunLogsResponse wraps a single RunLogLine; same rationale as StreamRunStatusResponse - room for heartbeats / metadata without breaking the stream contract, and satisfies RPC_RESPONSE_STANDARD_NAME.
| Field | Type | Label | Description |
|---|---|---|---|
| line | RunLogLine | line is the log entry. |
StreamRunStatusRequest
StreamRunStatusRequest subscribes to live status transitions for one run.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the workflow run UUID to subscribe to (required). |
StreamRunStatusResponse
StreamRunStatusResponse wraps a single NodeStatusUpdate so the stream envelope can grow new fields (e.g. heartbeat, run-level rollup) without a breaking proto change. The wrapper is also what buf v2 lint expects for streaming RPC response naming (RPC_RESPONSE_STANDARD_NAME).
| Field | Type | Label | Description |
|---|---|---|---|
| update | NodeStatusUpdate | update is the per-node status transition. |
StreamVisibleRunsRequest
StreamVisibleRunsRequest opens the caller-scoped live run stream (QWP-3,
#2359). It carries no run/workflow id: the caller's allow-set (team units +
owned private workflows) is materialized server-side from app.member_id at
subscribe. status_filter selects which runs the snapshot replays and which
live transitions surface as "in the active set" (a run leaving the filtered
set emits a leaving event so the panel drops it).
| Field | Type | Label | Description |
|---|---|---|---|
| status_filter | RunStatusFilter | status_filter narrows the snapshot + the live active set; see RunStatusFilter. The panel defaults to RUN_STATUS_FILTER_ACTIVE. |
ToolCatalogAction
ToolCatalogAction is one whitelisted operation on a tool, with the argument
names a tool_action step supplies under with. #2067.
| Field | Type | Label | Description |
|---|---|---|---|
| action | string | action is the operation name (e.g. "get_issue"). | |
| description | string | description is a short human-readable label for the action. | |
| args | ToolCatalogArg | repeated | args are the arguments the action accepts under the step's with block. |
ToolCatalogArg
ToolCatalogArg describes one argument a tool_action with block may set. #2067.
| Field | Type | Label | Description |
|---|---|---|---|
| name | string | name is the with key (e.g. "repo", "number", "body"). | |
| type | string | type is the JSON value type: one of string | |
| required | bool | required reports whether the action requires this argument. | |
| description | string | description is a short human-readable label for the argument. | |
| pinned_literal | bool | pinned_literal marks a SECURITY-PINNED argument whose with value MUST be an author-committed literal string, never a ${...} runtime template (the WF-30 repo pin). The create-time validator enforces this generically off the SAME catalog flag, so a future pinned arg needs only the flag set — and the client step composer drives its literal-only block from this field instead of inferring it by arg name (#2084). Default false = templatable (today's behaviour for every non-repo arg). #2084. |
ToolCatalogEntry
ToolCatalogEntry is one whitelisted governed tool and the actions it exposes to a tool_action step. #2067.
| Field | Type | Label | Description |
|---|---|---|---|
| tool | string | tool is the governed tool namespace (e.g. "github"). | |
| description | string | description is a short human-readable label for the tool. | |
| actions | ToolCatalogAction | repeated | actions are the whitelisted operations this tool exposes. |
TranscriptEvent
TranscriptEvent is one captured turn event (assistant message, tool call, or tool result), redacted per the caller's clearance.
| Field | Type | Label | Description |
|---|---|---|---|
| session_id | string | session_id is the agent session this event belongs to. A node re-run by an acceptance redo produces several sessions (attempts) for the same node; this field is preserved so the client can group events by attempt. | |
| seq | int32 | seq is the monotonic capture order WITHIN a session_id (stable render order for one attempt). It is NOT globally unique across attempts — group by session_id first, then order by seq. | |
| event_type | string | event_type is one of: assistant_message | |
| content | google.protobuf.Struct | content is the event payload, with sensitive fields redacted per the caller's clearance (same discipline as GetNodeIO): assistant_message: { text } tool_call: { tool_name, tool_call_id, arguments } tool_result: { tool_call_id, tool_name, result, is_error, duration_ms } | |
| created_at | google.protobuf.Timestamp | created_at is when the event was captured. |
TriggerWorkflowRequest
TriggerWorkflowRequest starts workflow execution.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow_id | string | workflow_id is the workflow UUID to execute (required). | |
| context | google.protobuf.Struct | context is optional execution context data. |
TriggerWorkflowResponse
TriggerWorkflowResponse contains the started workflow run.
| Field | Type | Label | Description |
|---|---|---|---|
| run | WorkflowRun | run is the created workflow run. |
UpdateWorkflowDefinitionRequest
UpdateWorkflowDefinitionRequest edits a workflow's stored definition. #2002.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow_id | string | workflow_id is the workflow UUID (required). | |
| definition | google.protobuf.Struct | definition is the new workflow definition JSON (required, validated against the same schema + semantic fences as CreateWorkflow). |
UpdateWorkflowDefinitionResponse
UpdateWorkflowDefinitionResponse returns the workflow with its edited definition and bumped updated_at. #2002.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow | Workflow | workflow is the updated workflow. |
UpdateWorkflowStatusRequest
UpdateWorkflowStatusRequest transitions a workflow's lifecycle status. WF-24 (#1888).
| Field | Type | Label | Description |
|---|---|---|---|
| workflow_id | string | workflow_id is the workflow UUID (required). | |
| status | string | status is the target lifecycle status: draft |
UpdateWorkflowStatusResponse
UpdateWorkflowStatusResponse returns the workflow with its new status.
| Field | Type | Label | Description |
|---|---|---|---|
| workflow | Workflow | workflow is the updated workflow. |
UserPromptFrame
UserPromptFrame is the live mid-run elicitation payload of a NodeIOFrame. It is structural (no free text is redacted) so the "answer needed" prompt surfaces at every clearance; answering flows through the (LBE-B) AnswerRunPrompt RPC keyed on prompt_id. The full question/options detail is fetched via GetRunPrompt.
| Field | Type | Label | Description |
|---|---|---|---|
| prompt_id | string | prompt_id is the governance_approvals row id the answer is recorded against (== the node's blocked_by uuid). The client fetches detail via GetRunPrompt. | |
| question | string | question is the prompt headline (already model-authored, not free-text tool output — surfaced at every clearance so the card is legible). | |
| asked_by_step | string | asked_by_step is the agent_action step whose agent is posing the question (LBE-B3, #2292): either the DSL-declared asked_by or the last-completed agent step on the run's executed path. Empty on a pre-B3 run or a run with no agent turn yet. Structural — surfaced at every clearance so the card renders which agent is asking without a GetRunPrompt lookup. | |
| asked_by_role | string | asked_by_role is that step's agent_role — the human-legible identity of the asking agent (e.g. "eng", "reviewer"). Empty when unattributed. |
VisibleRunEvent
VisibleRunEvent is one caller-scoped run delta (QWP-3, #2359). It is STRUCTURAL ONLY — every field is chrome the panel renders a row from, at EVERY clearance; NO free text / token content rides this event (that stays gated on StreamNodeIO). A snapshot event and a live event share this shape; is_snapshot distinguishes replay from live, and snapshot_done marks the boundary.
| Field | Type | Label | Description |
|---|---|---|---|
| run_id | string | run_id is the workflow run this event describes. | |
| workflow_id | string | workflow_id is the run's parent workflow. | |
| workflow_name | string | workflow_name is the parent workflow's display name (structural label). | |
| status | string | status is the public RUN status: pending | |
| nodes_total | int32 | nodes_total is the run's total action count (populated on snapshot rows; a live status delta derived from the StatusHub tee leaves it 0 and the client keeps its last-known count — same zero-default convention as WorkflowRun.nodes_total). | |
| nodes_done | int32 | nodes_done is the count of actions in a terminal status — the same set as WorkflowRun.nodes_done ('approved' | |
| needs_input | bool | needs_input is true when the run is parked on a human user_input prompt (a governance_approvals row with action_type='user_prompt' open on the run). Derived from the StatusHub org tee, NOT from a per-run wfio subscription. | |
| needs_approval | bool | needs_approval is true when the run is parked on an approval/question gate (a governance_approvals row with action_type='approval_gate' | |
| asked_by_role | string | asked_by_role is the human-legible role of the agent posing the prompt/gate (e.g. "eng", "reviewer"), when known. Structural attribution — surfaces at every clearance. Empty when unattributed. | |
| prompt_id | string | prompt_id is the governance_approvals row id the panel deep-links to for the prompt/approval detail (GetRunPrompt / RecordDecision). Empty when the run is not currently parked on a prompt or gate. | |
| leaving | bool | leaving is true when this run has LEFT the caller's filtered active set (it reached a terminal status, or a status filter no longer matches). The panel drops the row. Mutually exclusive with a live status update on the same run. | |
| is_snapshot | bool | is_snapshot is true on the replay events emitted before snapshot_done; false on every live delta after it. | |
| snapshot_done | bool | snapshot_done is true on the single marker event that ends the snapshot replay and precedes the first live delta. All other fields are unset on the marker. | |
| display_name | string | display_name is the run's human-readable title (workflow_runs.display_name, core#2457) — the founder-typed task title for a direct-task run. Structural run metadata, same class as workflow_name (surfaces at every clearance; NO agent I/O free text rides this event). Populated on SNAPSHOT rows (read from the run store); a live status delta derived from the StatusHub tee leaves it EMPTY and the client keeps its last-known title — the same populate-on- snapshot / empty-on-live-delta convention as nodes_total/nodes_done. The panel's display fallback is display_name -> workflow_name -> run id. | |
| prompt_kind | RunPromptKind | prompt_kind is the kind of human-answerable gate this run is parked on (core#2520). It is the discriminator needs_input / needs_approval cannot give: both gate kinds report needs_approval, and only this field separates a two-button confirm from an N-way structured question. |
SET ON BOTH ARMS, and co-derived with the two booleans from the SAME action_type on each — so prompt_kind != UNSPECIFIED holds exactly when needs_input || needs_approval, on a snapshot row and on a live delta alike. (That is the deliberate contrast with status, whose two arms carry two different vocabularies — core#2521. Do not add another such field.)
WHICH ROW EACH ARM DESCRIBES: on a snapshot row it is the FRESHEST open governance row on the run (a DISTINCT ON (run_id) reduction — see open_prompt_count for the ones it masks); on a live delta it is the action that transition is about, when it parked awaiting_approval, and UNSPECIFIED on any other transition — the same "prompt answered / gate resolved" clear that drops needs_input / needs_approval. |
| open_prompt_count | int32 | optional | open_prompt_count is how many OPEN (status='pending') human-answerable gates the run holds — the same action_type set prompt_kind is drawn from. A count > 1 is the row's "and N more" disclosure over the DISTINCT ON reduction; the parallel shapes shipped by WR2-2 make >1 the designed case, not a rarity.
POPULATED ON SNAPSHOT ROWS ONLY. The live delta rides the StatusHub org tee, which delivers ONE action row per event; a run-wide count would need a per-event DB round-trip, which the #2347 substrate ratification rules out. A live delta therefore leaves it ABSENT and the client keeps its last-known count until the next snapshot (the resync tick re-snapshots on a fixed interval, so the staleness is bounded) — the same populate-on-snapshot discipline as nodes_total / nodes_done / display_name.
EXPLICIT PRESENCE, deliberately, and NOT the zero-means-unknown convention its neighbours use: 0 is a meaningful and common value here (a run that just had its last gate resolved genuinely has 0), so a plain int32 could not tell "no gates open" from "this arm does not carry it" from "the read failed and the snapshot degraded". ABSENT = not determined on this message; PRESENT = authoritative, including present-and-0. Read it only in conjunction with is_snapshot, and treat its presence as the signal that prompt_kind on this message is authoritative too. |
| last_action_status | string | | last_action_status is the ACTION 5-colour palette value (queued | running | done | failed | blocked, the workflow_actions.status fold — see NodeStatusUpdate.status) for the ONE action this LIVE delta is about. It is the value that, before core#2521, was overloaded onto status on the live arm and mis-documented as a run status; it now has its own field so status can mean the same thing on both arms. POPULATED ON LIVE DELTAS ONLY — a snapshot row describes a whole run, not a single action, and leaves this EMPTY. It is a per-delta HINT, not authoritative run state: the run-level truth is status (the run status) and is_waiting (the wait-park roll-up). A client that only renders run-level chrome can ignore it. |
| is_waiting | bool | | is_waiting is true when the run is parked on a WR2-4 wait node — a node suspended on an external signal or a re-evaluated condition (workflow_actions.status='waiting', migration 186). It is the run-level roll-up of the per-node WorkflowAction.blocked_by wait discriminator (core#2519), and it exists because a wait keeps workflow_runs.status='running' by design (interpreter/wait.go): without this flag status alone cannot tell a wait-parked run from a working one on a cold list load, which is the core#2521 gap.
SET ON BOTH ARMS, the same shape needs_input / needs_approval establish: on a snapshot row it is the run-level roll-up (the run has at least one node currently in waiting); on a live delta it reflects whether THIS action parked on a wait — the same per-action granularity the needs_* booleans carry on the live arm, cleared by the same "wait resolved" transition that advances the node. So a live delta carrying a wait-park and a subsequent cold snapshot of that run AGREE that the run is waiting, which is the invariant core#2521 restores. A plain bool (not optional) deliberately: false is a meaningful, common, authoritative value on both arms — unlike open_prompt_count, is_waiting is carried on the live arm, so there is no absent-vs-false ambiguity to encode. |
VisibleRunRow
VisibleRunRow is the panel's list-row shape: the run plus the parent workflow's display name so the panel labels the row without a second fetch (QWP-3, #2359). The run body reuses the frozen WorkflowRun contract (nodes_total/nodes_done are populated, exactly as ListRuns); workflow_name is carried alongside because WorkflowRun itself does not hold the workflow's name.
| Field | Type | Label | Description |
|---|---|---|---|
| run | WorkflowRun | run is the WorkflowRun (id, workflow_id, status, node counts, timestamps). WorkflowRun.display_name also carries the run title (#2457); it is duplicated onto this wrapper's display_name for parity with workflow_name below. | |
| workflow_name | string | workflow_name is the parent workflow's display name (workflows.name). | |
| display_name | string | display_name is the run's human-readable title (workflow_runs.display_name, core#2457) — the founder-typed task title for a direct-task run. Structural run metadata, same class as workflow_name; the panel's display fallback is display_name -> workflow_name -> run id. Empty for non-direct-task runs. | |
| prompt_kind | RunPromptKind | prompt_kind is the kind of human-answerable gate this run is parked on — the FRESHEST open one when several are open (see open_prompt_count). core#2520. |
A list row deliberately carries NO prompt_id: it is a label/severity surface, not an answering surface, and the actionable id arrives on the stream snapshot (VisibleRunEvent.prompt_id). A client MUST NOT synthesize one. prompt_kind alone subsumes the needs_input / needs_approval pair that VisibleRunEvent carries — USER_PROMPT is needs_input, the two gate kinds are needs_approval — so the list row still gets the whole "does this need a human, and for what" answer, more precisely than the booleans give it. | | open_prompt_count | int32 | optional | open_prompt_count is how many OPEN (status='pending') human-answerable gates this run holds — the same action_type set prompt_kind is drawn from, so a gate a viewer cannot answer from this surface is never counted. It exists because prompt_kind/prompt_id are a DISTINCT ON (run_id) reduction to the freshest row, which with WR2-2 parallel shipped silently masks the others; a count > 1 is the row's "and N more" disclosure.
EXPLICIT PRESENCE, deliberately: 0 is a meaningful and common value here (most runs hold no gate), so the zero-means-not-carried convention that nodes_total/nodes_done use would make a failed or skipped read indistinguishable from a confident "nothing is waiting". ABSENT means "not determined on this message"; present means authoritative — including present-and-0. It is therefore also the disambiguator for prompt_kind == UNSPECIFIED. |
| is_waiting | bool | | is_waiting is true when the run is parked on a WR2-4 wait node — the same run-level roll-up VisibleRunEvent.is_waiting carries (core#2521), computed from the same source (the run has at least one node in workflow_actions.status= 'waiting', migration 186). It exists because a wait keeps workflow_runs.status='running' by design, so this cold list row would otherwise show a parked run as indistinguishable from a working one. Structural severity chrome, same class as prompt_kind. A plain bool: false is authoritative here. |
Workflow
Workflow represents a workflow definition.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | id is the workflow UUID. | |
| org_id | string | org_id is the organization UUID. | |
| team_id | string | team_id is the optional team UUID. | |
| name | string | name is the workflow name. | |
| description | string | description is the workflow description. | |
| definition | google.protobuf.Struct | definition is the workflow definition JSON. | |
| created_by | string | created_by is the creator UUID. | |
| status | string | status is the workflow status (draft, active, archived). | |
| created_at | google.protobuf.Timestamp | created_at is the creation timestamp. | |
| deleted_at | google.protobuf.Timestamp | deleted_at is the optional deletion timestamp. | |
| trigger_type | string | trigger_type is how the workflow is triggered: "manual" (default) or "schedule". Backed by workflows.trigger_type (migration 151, WF-23 #1887). | |
| enabled | bool | enabled gates schedule firing (W4). Always false for a manual workflow, and false for any workflow until an operator opts in via SetWorkflowEnabled. Backed by workflows.enabled (migration 151); a behavioral no-op until W4. | |
| last_run_status | string | last_run_status is the status of this workflow's most recent run ("" when it has never run). ROLLUP field — WF-25 computes it; this RPC (WF-24 #1888) lays the field and returns the zero value. | |
| last_run_at | google.protobuf.Timestamp | last_run_at is when this workflow's most recent run started. ROLLUP — WF-25 computes it; unset here. | |
| run_count | int32 | run_count is the total number of runs this workflow has had. ROLLUP — WF-25 computes it; 0 here. | |
| avg_cost_usd | double | avg_cost_usd is the mean rolled-up cost across this workflow's runs. ROLLUP — WF-25 computes it; 0 here. USD as double (analytics.proto convention). | |
| updated_at | google.protobuf.Timestamp | updated_at is the last-modification timestamp, maintained by a BEFORE UPDATE trigger (migration 166) that stamps now() on every UPDATE — definition edits (UpdateWorkflowDefinition, #2002), status changes, enable toggles, and the soft-delete write. A client compares it against a cached value to detect staleness. Backfilled to created_at for pre-migration rows so an untouched workflow never looks freshly modified. | |
| visibility | string | visibility is the workflow's audience, backed by workflows.visibility (migration 187): "team" (default — visible to its team) |
WorkflowAction
WorkflowAction represents an individual step within a workflow run.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | id is the action UUID. | |
| run_id | string | run_id is the parent run UUID. | |
| org_id | string | org_id is the organization UUID. | |
| step_name | string | step_name is the workflow step name. NOT UNIQUE within a run: a step inside a bounded loop's body projects ONE ROW PER LAP, all sharing this name — see iteration below, which is what separates and orders them. | |
| agent_id | string | agent_id is the assigned agent UUID. | |
| status | string | status is the RAW workflow_actions.status value. This is the FIRST of the two status vocabularies on this wire and clients have been guessing at it, so it is written out here in full. The closed set is exactly the ten values migration 186's workflow_actions_status_check admits (014 → 147 skipped → 186 waiting): |
pending = queued, not yet dispatched; in_progress = executing; awaiting_approval = parked on a governance row (an approval gate OR a WR2-5 question); waiting = parked on a WR2-4 wait node (external event / poll). approval_id is NEVER stamped on a waiting row — and note that is NOT because no governance row can exist (a POLL wait runs the same per-call self-gate machinery a tool_action does, so it absolutely can open one). It is because a wait's gate resolves through the step-approved event, which carries no approval id, and the only writer of approval_id — the step-blocked event — has no caller in the wait arm. That invariant is LOAD-BEARING: the wait sub-kinds are not in the blocked-sub-kind set, so blocked_by resolves for them only via the fallback that requires an empty approval_id. Break the invariant upstream and a parked wait would deep-link to an approval (rationale corrected in core#2514); escalated = parked, escalated to a higher clearance; approved = gate approved (terminal for the gate node); completed = finished successfully; skipped = an un-taken conditional arm (migration 147), resolved and never going to run; rejected = gate denied; failed = errored.
The SECOND vocabulary is NodeStatusUpdate.status (the live-DAG stream), which is a FROZEN five-colour palette these ten fold into: pending -> queued; in_progress -> running; awaiting_approval | waiting | escalated -> blocked; approved | completed | skipped -> done; rejected | failed -> failed.
Do not widen the palette; branch on blocked_by (below) to tell the three kinds of park apart. The fold lives in internal/workflow/projection.go (publicStatusForAction) and is the single source of that mapping. |
| input | google.protobuf.Struct | | input is the step input data. |
| output | google.protobuf.Struct | | output is the step output data. |
| decided_by | string | | decided_by is the decision maker UUID. |
| decided_at | google.protobuf.Timestamp | | decided_at is the decision timestamp. |
| created_at | google.protobuf.Timestamp | | created_at is the creation timestamp. |
| approval_id | string | | approval_id is the governance_approvals row this step is waiting on, populated when the engine creates an approval as part of an approval_gate / escalated step. Empty otherwise. The frontend uses this to deep-link the live-DAG inspector to /approvals/<id> (#1109). |
| cost_usd | double | | cost_usd is the rolled-up cost charged to this action (LLM tokens + tool execution). Convention matches analytics.proto: USD as double (#1111). Populated by engine on transition; see #1127 (engine cost-meter wiring is the follow-up that actually writes non-zero values). Until #1127 lands this field is always 0. |
| elapsed_ms | int64 | | elapsed_ms is the wall-clock time spent in in_progress for this action. For terminal actions this is the final duration. Engine updates on every transition (#1111). |
| blocked_by | string | | blocked_by says WHY this node is parked. It is the SAME PROJECTED TOKEN as NodeStatusUpdate.blocked_by (field 6 of that message) — same closed sub-kind vocabulary, same precedence, same "empty unless the node is parked" rule — so a client reads it identically whichever surface it arrived on. Both are rendered by one function (internal/workflow/projection.go, projectBlockedBy); read that message's doc for the branch rule. Values today: "awaiting-question" = a WR2-5 question node waiting on a structured human answer (a sub-kind discriminator; wins over the uuid); "waiting-on-signal" = a WR2-4 wait node suspended on an external event; "waiting-on-poll" = a wait node re-evaluating a condition on a schedule; a UUID = an ordinary approval gate, the governance_approvals row to deep-link (also on approval_id, field 12); any other free text = a legacy "Awaiting Dana W." label.
Empty whenever the node is not parked, INCLUDING for a resolved wait/question: the underlying column is never cleared on resolve, so this projection gates on status and a completed node reports empty. Never infer parked-ness from this field's presence — branch on status (the fold above) first, exactly as the stream does.
core#2513: added because StreamRunStatus emits only on TRANSITION, so a run already parked when the page loads never receives a NodeStatusUpdate and a cold read could not tell an approval gate from a question from a wait. | | iteration | int32 | | iteration is the loop-lap ordinal (WR2-2, migration 176): 0 for every non-loop step AND for the loop CONTAINER node itself; the 1-based lap number for a step executed inside the loop's body. The projection upsert key is (run_id, step_name, iteration), so an N-lap loop body emits N SIBLING ROWS HERE with the same step_name — this field is the only thing that disambiguates and orders them. Actions arrive ordered by (created_at, iteration, id).
Same field as NodeStatusUpdate.iteration (field 8), same semantics.
core#2513: added for the same cold-load reason as blocked_by — a lap already in flight emits no transition, so page load could not render rounds at all. |
WorkflowRun
WorkflowRun represents a workflow execution instance.
| Field | Type | Label | Description |
|---|---|---|---|
| id | string | id is the run UUID. | |
| workflow_id | string | workflow_id is the parent workflow UUID. | |
| org_id | string | org_id is the organization UUID. | |
| triggered_by | string | triggered_by is the trigger user UUID. | |
| status | string | status is the run status: pending | |
| context | google.protobuf.Struct | context is the execution context data. | |
| started_at | google.protobuf.Timestamp | started_at is the execution start timestamp. | |
| completed_at | google.protobuf.Timestamp | completed_at is the execution completion timestamp. | |
| created_at | google.protobuf.Timestamp | created_at is the creation timestamp. | |
| cost_usd | double | cost_usd is the rolled-up cost across every action in this run. Populated by engine on transition; see #1127 (engine cost-meter wiring is the follow-up that actually writes non-zero values). Until #1127 lands this field is always 0. | |
| paused_at | google.protobuf.Timestamp | paused_at is set when the run was last paused via PauseRun (#1110); cleared on resume. | |
| cancelled_at | google.protobuf.Timestamp | cancelled_at is set when the run was cancelled via CancelRun (#1110). | |
| nodes_total | int32 | nodes_total is the count of workflow_actions rows for this run. Populated by ListRuns (#1074); other RPCs leave it at 0 since they surface action lists directly. Convention: -1 is never used; an unpopulated total is 0 and the consumer falls back to the actions array length when present. | |
| nodes_done | int32 | nodes_done is the count of workflow_actions in a TERMINAL status for this run — precisely the raw statuses that fold to done or failed on the NodeStatusUpdate palette (see WorkflowAction.status for both vocabularies): 'approved', 'completed', 'skipped', 'rejected', 'failed'. |
'skipped' is an un-taken conditional arm (migration 147): resolved, and it will never run, so it folds to done. It was missing from this count until core#2513, which is why a run with a conditional could never reach 100 %.
'waiting' and 'awaiting_approval' / 'escalated' are DELIBERATELY EXCLUDED: they are non-terminal parks that WILL transition again, so counting them as done would report an indefinitely parked run as complete.
THERE IS NO nodes_waiting COUNTER, and the reason is STALENESS, not informativeness (rationale corrected in core#2514; the earlier wording here was wrong twice — it argued against folding parks into this numerator, which nobody proposed, and claimed actions[].blocked_by is a strictly better substitute, which is false on a LIST surface that carries no actions[] at all).
The reason that actually holds: nodes_total / nodes_done are populate-on-snapshot and zero-on-live-delta by documented convention (the live projection leaves them 0), and a wait park emits NO run-status transition — deliberately. A nodes_waiting would inherit that convention and so be correct at subscribe and permanently stale thereafter: "a park produces no frame", the exact failure core#2513 exists to close, reintroduced one message over. A counter that is right once and then silently wrong is worse than one that is absent.
The list-surface gap that observation points at is real and is tracked as #2521 (VisibleRunEvent.status carries two disjoint vocabularies selected by is_snapshot); the fix there is one vocabulary per field plus a structural park-kind flag on both arms, not a counter here. Note run.status stays 'running' while a wait node is parked (nothing is being asked of a human), by design.
Populated by ListRuns (#1074). Same zero-default semantics as nodes_total. | | display_name | string | | display_name is the run's human-readable title (workflow_runs.display_name, core#2457) — the founder-typed task title stamped by the coordinator Temporal runner for direct-task runs. It is run METADATA (the same class as the workflow name), NOT agent I/O: the StreamNodeIO clearance floor gates agent output free text, not run titles. Empty for pre-2457 runs and every non-direct-task run; clients fall back to the workflow name then the run id. |
RunPromptKind
RunPromptKind is the SERVED discriminator for the human-answerable gate a run is currently parked on (core#2520, WR2 plan D2). It is carried on VisibleRunRow and on BOTH arms of VisibleRunEvent.
WHY IT EXISTS: needs_input / needs_approval fold approval_gate and
question_gate into ONE boolean, so a client holding a parked row cannot tell
a two-button confirm from an N-way structured question — and the two differ in
write path (RecordDecision with a {option_key, fields} response_payload vs a
plain decision), in clearance gating (question_gate is clearance-gated,
user_prompt deliberately is not) and in routing (N-way vs a single next).
A card that picks its write path by guessing is a race; this field removes the
guess.
NEVER INFER IT — not from asked_by_role, not from the presence of a prompt spec, not from a GetRunPrompt probe. The same row is reachable from the DAG, the Quad dock and the Approvals list.
THE VALUE NAMES ARE THE governance_approvals.action_type SPELLING, verbatim,
and that is deliberate: GetRunPromptResponse.kind (core#2514) already serves
this same discriminator as the raw action_type string, so a client can compare
the two fields for one row directly. A second, prettier vocabulary for a
single discriminator is the defect core#2521 documents on the neighbouring
status field; do not introduce another. RUN_PROMPT_KIND_QUESTION_GATE is
what a client maps to its own needs-answer severity — that mapping belongs
to the client's presentation layer, not to this wire.
| Name | Number | Description |
|---|---|---|
| RUN_PROMPT_KIND_UNSPECIFIED | 0 | RUN_PROMPT_KIND_UNSPECIFIED means the run holds NO open human-answerable gate — or, on a degraded read, that the server could not determine one. The two are distinguished by open_prompt_count's PRESENCE, not by this value. |
| RUN_PROMPT_KIND_USER_PROMPT | 1 | RUN_PROMPT_KIND_USER_PROMPT is the LBE-B1 free-text/choice prompt (action_type='user_prompt'). Equivalent to needs_input=true. Answered via WorkflowService.AnswerRunPrompt as {choice, text}. Deliberately NOT clearance-gated. |
| RUN_PROMPT_KIND_APPROVAL_GATE | 2 | RUN_PROMPT_KIND_APPROVAL_GATE is an ordinary approve/reject gate (action_type='approval_gate'). Equivalent to needs_approval=true. Answered via ApprovalService.RecordDecision with a bare decision. |
| RUN_PROMPT_KIND_QUESTION_GATE | 3 | RUN_PROMPT_KIND_QUESTION_GATE is the WR2-5 structured question (action_type='question_gate'). ALSO reports needs_approval=true — the boolean cannot distinguish it, which is why this enum exists. Answered via ApprovalService.RecordDecision with an APPROVED decision whose response_payload is {option_key, fields}; read the spec with GetRunPrompt. |
RunStatusFilter
RunStatusFilter selects which subset of WorkflowRun rows ListRuns returns. Mirrors the four buckets the redesigned Workflows page renders in its segmented control. UNSPECIFIED maps to ALL (no filtering) so an unset filter on the wire returns the most-permissive response, matching proto3 default-zero ergonomics.
GAP-31 (#1074).
| Name | Number | Description |
|---|---|---|
| RUN_STATUS_FILTER_UNSPECIFIED | 0 | RUN_STATUS_FILTER_UNSPECIFIED returns runs in any status (== ALL). |
| RUN_STATUS_FILTER_ACTIVE | 1 | RUN_STATUS_FILTER_ACTIVE returns runs where status IN ('pending', 'running', 'paused'). |
| RUN_STATUS_FILTER_COMPLETED | 2 | RUN_STATUS_FILTER_COMPLETED returns runs where status = 'completed'. |
| RUN_STATUS_FILTER_FAILED | 3 | RUN_STATUS_FILTER_FAILED returns runs where status IN ('failed', 'cancelled'). Cancelled is grouped with failed because the dashboard surfaces both as "did not complete successfully". |
| RUN_STATUS_FILTER_ALL | 4 | RUN_STATUS_FILTER_ALL returns runs in any status. Semantically the same as UNSPECIFIED; provided so callers can be explicit. |
WorkflowService
WorkflowService provides lifecycle management for workflows, execution triggering, and approval chain operations.
2026-04-26 redesign extension (tracker #1031): adds streaming RPCs for live-DAG rendering (StreamRunStatus #1046, StreamRunLogs #1107), clearance-gated step I/O (GetNodeIO #1108), an approval_id deep-link on WorkflowAction (#1109), idempotent run-control RPCs (PauseRun + CancelRun #1110), and per-action / per-run cost tracking fields (#1111).
| Method Name | Request Type | Response Type | Description |
|---|---|---|---|
| CreateWorkflow | CreateWorkflowRequest | CreateWorkflowResponse | CreateWorkflow creates a new workflow with definition validation. |
| GetWorkflow | GetWorkflowRequest | GetWorkflowResponse | GetWorkflow retrieves a workflow by ID. |
| ListWorkflows | ListWorkflowsRequest | ListWorkflowsResponse | ListWorkflows lists workflows for an organization with filtering. |
| TriggerWorkflow | TriggerWorkflowRequest | TriggerWorkflowResponse | TriggerWorkflow starts execution of a workflow. |
| GetWorkflowStatus | GetWorkflowStatusRequest | GetWorkflowStatusResponse | GetWorkflowStatus retrieves the current status of a workflow run. |
| ApproveAction | ApproveActionRequest | ApproveActionResponse | ApproveAction approves a workflow action awaiting approval. |
| RejectAction | RejectActionRequest | RejectActionResponse | RejectAction rejects a workflow action awaiting approval. |
| EscalateAction | EscalateActionRequest | EscalateActionResponse | EscalateAction escalates a workflow action to higher clearance. |
| ListPendingApprovals | ListPendingApprovalsRequest | ListPendingApprovalsResponse | ListPendingApprovals lists workflow actions awaiting approval. |
| StreamRunStatus | StreamRunStatusRequest | StreamRunStatusResponse stream | StreamRunStatus opens a server-streaming RPC that emits a NodeStatusUpdate every time a workflow_action row in the given run transitions (queued -> running -> done |
Tenant isolation: the stream's org_id is taken from the JWT-derived ambient scope. Events for any other tenant are dropped before reaching the wire. The Hub also validates the requested run_id belongs to the caller's tenant on subscribe.
GAP-29 (#1046). |
| StreamRunLogs | StreamRunLogsRequest | StreamRunLogsResponse stream | StreamRunLogs opens a server-streaming RPC that emits RunLogLine messages as workflow execution writes log entries. Backed by the Postgres LISTEN/NOTIFY channel workflow_run_logs (migration 086).
GAP-30 (#1107). | | GetNodeIO | GetNodeIORequest | GetNodeIOResponse | GetNodeIO returns the input and output JSON of a single workflow_action, with sensitive fields redacted based on the caller's clearance level (compared against the workflow's data_classification - workflow_actions is registered as Operational+High in classregistry).
The redacted_fields list names every top-level key that was stripped from input or output so the UI can render a "redacted" placeholder rather than silently lying about completeness.
GAP-32 (#1108). | | GetNodeTranscript | GetNodeTranscriptRequest | GetNodeTranscriptResponse | GetNodeTranscript returns the agent's TURN TRANSCRIPT for one node — the intermediate reasoning/tool trail (assistant messages, tool calls, tool results) that FoldStream discards after keeping only the final output. Reads the session_events captured at fold time (migration 160, #1969 PR-1) by (run_id, step_name), org-scoped and run-scoped fail-closed exactly like GetNodeIO. Content is clearance-redacted on READ via the same RedactStruct discipline (raw at rest, redact at the boundary).
A node re-executed by an acceptance redo (#1966) has MULTIPLE agent sessions (attempts) for the same (run_id, step_name); this RPC returns ALL of their events with each event's session_id preserved, so the client can group the transcript by attempt without losing history.
#1969 PR-3. | | StreamNodeIO | StreamNodeIORequest | StreamNodeIOResponse stream | StreamNodeIO streams a run's agent-step LLM I/O: a snapshot of the already- captured transcript events (clearance-redacted, from session_events, the same read+redact path GetNodeTranscript uses), then a snapshot_done marker, then LIVE frames coalesced from the worker as the agent thinks/acts.
The live frames arrive over a new in-memory NodeIOHub in the context-engine, fed by the Redis pub/sub channel wfio:{org}:{run} the orchestrator publisher tees (LBE-A1). Subscribe-before-snapshot ordering (#1962): the hub subscription opens FIRST so no frame fired during the snapshot read is lost; the client dedups the snapshot↔live overlap by (session_id, seq).
Live redaction floor (byte-consistent with post-hoc GetNodeTranscript): a caller with clearance >= 4 receives full frames including token_text; a caller with clearance < 4 receives STRUCTURAL frames only (status, tool_call name/id, tool_result is_error/timing, user_prompt) — token_text is suppressed server-side in the CE adapter, exactly what GetNodeTranscript would redact for that clearance.
Backpressure: the hub delivers to a per-subscriber buffered channel; on a full buffer (slow client) it REPLACES the overflow with a synthesized StreamGap so the client renders "stream resynced" and may refetch the snapshot rather than silently missing frames. The Redis-subscribe reader never blocks.
Additive to the frozen MVP contract. LBE-A2 (#2240), HLD #2233 §1.2. | | PauseRun | PauseRunRequest | PauseRunResponse | PauseRun marks the run as PAUSED at the next checkpoint. Idempotent - calling PauseRun on an already-paused or terminal run returns the current state without erroring. Audit-logged.
GAP-34 (#1110). | | CancelRun | CancelRunRequest | CancelRunResponse | CancelRun halts the run and marks status=CANCELLED. Idempotent. Audit-logged.
GAP-34 (#1110). |
| ResumeRun | ResumeRunRequest | ResumeRunResponse | ResumeRun lifts a pause on a workflow run, letting execution continue from the parked step. This is the Temporal-era counterpart to PauseRun: it sends the resume signal (internal/temporalwf/api.SignalResume) to the run's Temporal execution, which clears the interpreter's pause flag so the walk advances at the next boundary. Idempotent — resuming a run that is not paused is a harmless no-op signal.
Requires Temporal execution (TEMPORAL_ENABLED). On a deployment where Temporal is disabled — or against a terminal run — ResumeRun returns FAILED_PRECONDITION: the legacy engine has no resumable pause state, so there is nothing to resume. Additive to the frozen 16-RPC MVP contract (LLD #1798 §4), WF-16 (#1819). | | ListRuns | ListRunsRequest | ListRunsResponse | ListRuns returns past run metadata for a single workflow, ordered newest-started first. Cursor-paginated on (started_at DESC, id DESC). RLS-scoped — only runs owned by the caller's org are returned.
Powers the Dashboard "Active workflows" panel and the redesigned Workflows page run-history view (workflows.jsx). Per-node detail is intentionally out of scope (use GetWorkflowStatus / StreamRunStatus — GAP-29).
GAP-31 (#1074). |
| UpdateWorkflowStatus | UpdateWorkflowStatusRequest | UpdateWorkflowStatusResponse | UpdateWorkflowStatus transitions a workflow's lifecycle status (draft | active | archived). Additive to the frozen MVP contract (LLD #1798 §4). WF-24 (#1888). |
| SetWorkflowEnabled | SetWorkflowEnabledRequest | SetWorkflowEnabledResponse | SetWorkflowEnabled configures a workflow's trigger (manual | schedule + schedule config) and the enabled gate that W4 schedule firing reads (LLD #1798 §2.2 disable-race). Behavioral no-op until W4 wires schedules: the RPC writes workflows.enabled / trigger_type / trigger_config, but nothing reads them yet, and manual runs are unaffected. Additive to the frozen MVP contract. WF-24 (#1888), backed by the WF-23 (#1887) store surface. |
| UpdateWorkflowDefinition | UpdateWorkflowDefinitionRequest | UpdateWorkflowDefinitionResponse | UpdateWorkflowDefinition edits a workflow's stored definition (the typed DAG). The new definition is validated through the SAME SchemaValidator.ValidateDefinition path as CreateWorkflow — an invalid definition (unknown step type, dangling next/on_approve reference, or a fenced case such as the WF-30 tool_action literal-repo rule) is rejected with INVALID_ARGUMENT and the stored definition is left untouched.
Run-immunity semantics (documented contract): editing the definition does NOT affect in-flight runs. A Temporal-managed run boots its definition from the run's own Temporal history (the interpreter walks the DAG captured at StartWorkflow), so a mid-flight edit cannot mutate a running walk. Runs started AFTER the edit commits pick up the new definition. Each edit bumps workflows.updated_at (migration 166) so a client can detect staleness; the workflow id is stable (this is an in-place edit, not a create-new + archive-old versioning scheme).
Guarded exactly like CreateWorkflow: RLS scopes the write to the caller's org (a foreign/missing workflow reads as NotFound), and the same icsBypassOnly auth surface applies. #2002. |
| DeleteWorkflow | DeleteWorkflowRequest | DeleteWorkflowResponse | DeleteWorkflow soft-deletes a workflow: it stamps workflows.deleted_at = now(). After delete the workflow disappears from ListWorkflows / GetWorkflow (both already filter deleted_at IS NULL). Idempotent — deleting an already-deleted (undiscoverable) workflow returns NotFound, matching Get.
Run-data semantics (documented contract): a soft-delete does NOT purge the workflow's runs, actions, or logs. The audit trail is preserved (CLAUDE.md: every action is auditable). The runs become UNDISCOVERABLE through the workflow — ListWorkflows no longer surfaces the parent, so a client cannot reach ListRuns for it without an out-of-band workflow_id — but any run row already known by id remains readable by GetWorkflowStatus until a DB-admin retention sweep hard-deletes it. Hard-delete of run data stays a DB-admin operation; this RPC adds no destructive cascade.
Governed exactly like archive (UpdateWorkflowStatus): RLS scopes the write, same icsBypassOnly auth surface. #2002. |
| GetToolCatalog | GetToolCatalogRequest | GetToolCatalogResponse | GetToolCatalog returns the create-time tool_action whitelist (WF-30) — the (tool, action, args) surface a tool_action step may name — from the single Go source of truth shared with the definition validator. The client step composer reads it so it never hard-codes a copy of the whitelist that could drift from what CreateWorkflow accepts. Read-only, tenant-independent (the whitelist is a platform constant), additive to the frozen MVP contract. #2067. |
| GetRunPrompt | GetRunPromptRequest | GetRunPromptResponse | GetRunPrompt returns the detail of a mid-run user_input prompt (Track B elicitation, LBE-B2 #2249). prompt_id is the node's blocked_by uuid — the governance_approvals row the run parked on (action_type='user_prompt', migration 181). It projects the typed question the step posed (question, options[], allow_text, text_placeholder, default_choice) plus the row's required_clearance, expires_at, and status, and computes can_answer SERVER-SIDE for THIS caller — the clearance floor AND the answerable_by scope ("" | "anyone" | "requester") are evaluated against the authenticated identity, so the client renders a live "you may answer" affordance without duplicating the authz. The row is RLS-scoped to the caller's org: a foreign prompt_id reads as NotFound.
Additive to the frozen MVP contract. LBE-B2 (#2249), HLD #2233 §2. | | AnswerRunPrompt | AnswerRunPromptRequest | AnswerRunPromptResponse | AnswerRunPrompt records an answer to a mid-run user_input prompt (LBE-B2 #2249). It writes the answer envelope {choice, text, answered_by} through B1's authoritative FIRST-WRITE path (approval.Service.RecordDecision → Store.TryResolveWithPayload — the same CAS that flips the row to answered and fans the DecisionPublisher signal that RESUMES the parked run); it does NOT reinvent the record. Authz is enforced SERVER-SIDE before the write: the caller must clear the row's required_clearance floor AND satisfy its answerable_by scope (answerable_by="requester" ⇒ only the run's triggerer may answer). A second answer to an already-answered prompt is a NO-OP (first-write-wins): the RPC returns already_answered=true carrying the recorded answer rather than erroring. A foreign prompt_id is NotFound; a wrong-clearance / wrong-answerer caller is PermissionDenied.
⚠ A QUESTION GATE IS REFUSED HERE with InvalidArgument naming the right door (core#2514). GetRunPrompt serves both kinds — see GetRunPromptResponse.kind — but this request shape ({choice, text}) cannot carry a question's {option_key, fields{}}: recording it would write an envelope whose option_key is empty, and the run would route to the step's fallthrough next with the operator's actual choice silently discarded. A question is answered through ApprovalService.RecordDecision, which is where the answer pre-flight lives.
Additive to the frozen MVP contract. LBE-B2 (#2249), HLD #2233 §2. |
| DeliverWorkflowEvent | DeliverWorkflowEventRequest | DeliverWorkflowEventResponse | DeliverWorkflowEvent delivers an external event to a run parked on a signal-mode wait node (WR2-4 Slice 6, #2266; LLD #2253 §3.4). It signals the run's Temporal execution with external_event; the interpreter's drain moves the payload into the wait node's correlation slot and the parked wait resolves, binding the payload as the node's output envelope.
⚠ SCOPE — THIS IS THE CONSUME CONTRACT, NOT INGESTION (founder scope call on HLD #2223, LLD §0). The caller already KNOWS (run_id, event_key). Untrusted webhook INGESTION — a subscription registry that resolves an external correlation key to a waiting (run_id, event_key), HMAC verification, replay protection — is an explicit fast-follow and is OUT of scope. THIS RPC IS THE ENTIRE SEAM it attaches to: that adapter, when built, performs only the resolution and calls this RPC. No signal shape, no drain and no wait-node change is required for it.
IDEMPOTENCY IS BY delivery_id, NOT BY event_key. A wait CONSUMES its slot when it resolves, so a retried delivery that lands after the first was consumed could otherwise satisfy a LATER wait on the same event_key (the next lap of a loop body) — an advance on an event that never occurred twice. This RPC therefore burns (org, run_id, event_key, delivery_id) durably BEFORE signalling: a retry is a no-op that never reaches the run, and deduplicated is true on the response. delivery_id is REQUIRED (an empty one is INVALID_ARGUMENT, never a silent fall-through to the non-idempotent path).
AUTHORIZATION — state precisely what is enforced, because this is a frozen public contract and an overstated control here gets cited later as evidence the surface is covered. What gates this RPC is:
- an AUTHENTICATED principal, and 2. ORG-SCOPED ROW-LEVEL SECURITY: the run is read back through the request's RLS-bound scope transaction, so a run owned by another org reads zero rows and the caller gets NOT_FOUND — indistinguishable from "does not exist". Every org this handler then uses (the dedupe-ledger key, the audit row, the derived Temporal execution id) is that authoritative workflow_runs.org_id, NEVER ambient request input.
There is NO per-action RBAC on this RPC, and there is none on PauseRun / CancelRun / ResumeRun either: WorkflowService is mounted with the authenticate-only interceptor (no RBAC interceptor). So any authenticated caller in the owning org may deliver an event to that org's run, exactly as any such caller may pause, cancel or resume it. This RPC introduces no new authority — it is at parity with its siblings, no wider.
⚠ LLD #2253 O4 said this RPC "reuses the run-control entitlement". That premise was false — no such per-action entitlement exists — and the LLD is being corrected. Whether these run-control RPCs SHOULD carry one is tracked separately (#2362); a narrower event-delivery capability remains a future refinement, not something this contract already provides.
unknown / foreign run -> NOT_FOUND terminal run -> FAILED_PRECONDITION empty event_key -> INVALID_ARGUMENT empty delivery_id -> INVALID_ARGUMENT Temporal disabled -> FAILED_PRECONDITION
Additive to the frozen MVP contract. WR2-4 (#2266), LLD #2253 §3.4. | | ListVisibleRuns | ListVisibleRunsRequest | ListVisibleRunsResponse | ListVisibleRuns returns the runs whose parent workflow the CALLER may see — the Quad Workflows Panel's caller-scoped run list (QWP-3, #2359, HLD #2347). Visibility is two arms (HLD §A): a workflow whose team_id is one of the caller's team units, OR a private workflow the caller owns. A caller in NONE of a workflow's teams and NOT its owner sees NOTHING for it. Unlike ListRuns (single workflow) this lists ACROSS workflows; it is RLS-scoped to the caller's org (no cross-org leak) AND filtered by app.member_id (SET LOCAL per request). Cursor-paginated on (started_at DESC, id DESC) — same keyset as ListRuns. The panel defaults status_filter to ACTIVE. | | StreamVisibleRuns | StreamVisibleRunsRequest | VisibleRunEvent stream | StreamVisibleRuns is the live analogue of ListVisibleRuns: a snapshot of the caller's currently-visible runs, a snapshot_done marker, then live VisibleRunEvent deltas (status transition / a run entering or leaving the caller's active set / needs_input / needs_approval). It REUSES the existing StatusHub org tee (Subscribe(org,""), the single process-wide LISTEN on workflow_run_status) filtered per-subscriber against the caller's allow-set — it does NOT open a per-run wfio subscription (that is StreamNodeIO's job for the single selected run). VisibleRunEvent is STRUCTURAL only — name, status, node counts, needs_input/needs_approval, asked_by_role, prompt_id — surfacing at EVERY clearance; no free text / tokens ride this event. QWP-3 (#2359). |
Scalar Value Types
| .proto Type | Notes | C++ | Java | Python | Go | C# | PHP | Ruby |
|---|---|---|---|---|---|---|---|---|
| double | double | double | float | float64 | double | float | Float | |
| float | float | float | float | float32 | float | float | Float | |
| int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| uint32 | Uses variable-length encoding. | uint32 | int | int/long | uint32 | uint | integer | Bignum or Fixnum (as required) |
| uint64 | Uses variable-length encoding. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum or Fixnum (as required) |
| sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28. | uint32 | int | int | uint32 | uint | integer | Bignum or Fixnum (as required) |
| fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56. | uint64 | long | int/long | uint64 | ulong | integer/string | Bignum |
| sfixed32 | Always four bytes. | int32 | int | int | int32 | int | integer | Bignum or Fixnum (as required) |
| sfixed64 | Always eight bytes. | int64 | long | int/long | int64 | long | integer/string | Bignum |
| bool | bool | boolean | boolean | bool | bool | boolean | TrueClass/FalseClass | |
| string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string | String | str/unicode | string | string | string | String (UTF-8) |
| bytes | May contain any arbitrary sequence of bytes. | string | ByteString | str | []byte | ByteString | string | String (ASCII-8BIT) |