Skip to main content

Workflow API

Table of Contents​

Top

upsquad/workflow/v1/workflow.proto​

AnswerRunPromptRequest​

AnswerRunPromptRequest records an answer to a mid-run user_input prompt. LBE-B2 (#2249).

FieldTypeLabelDescription
prompt_idstringprompt_id is the governance_approvals row id to answer (required).
choicestringchoice 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.
textstringtext 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).

FieldTypeLabelDescription
recordedboolrecorded 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_answeredboolalready_answered is true when the prompt was already resolved (this call was a benign no-op — first-write-wins). recorded is then false.
promptGetRunPromptResponseprompt 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.

FieldTypeLabelDescription
action_idstringaction_id is the workflow action UUID (required).
commentstringcomment is optional approval comment.

ApproveActionResponse​

ApproveActionResponse contains the approved action result.

FieldTypeLabelDescription
actionWorkflowActionaction is the updated workflow action.
next_actionsWorkflowActionrepeatednext_actions are any new actions created as a result.

CancelRunRequest​

CancelRunRequest cancels a workflow run.

FieldTypeLabelDescription
run_idstringrun_id is the workflow run UUID to cancel (required).
reasonstringreason is an optional human-readable note recorded in the audit log.

CancelRunResponse​

CancelRunResponse returns the post-cancel run state.

FieldTypeLabelDescription
runWorkflowRunrun is the workflow run with status='cancelled' (or unchanged if the run was already terminal - idempotent).

CreateWorkflowRequest​

CreateWorkflowRequest creates a new workflow.

FieldTypeLabelDescription
namestringname is the workflow name (required).
descriptionstringdescription is an optional workflow description.
definitiongoogle.protobuf.Structdefinition is the workflow definition JSON (required, validated against schema).
team_idstringteam_id is optional team scoping.
visibilitystringvisibility is the workflow's audience: "team" (default when empty — visible to its team, today's universal behaviour)

CreateWorkflowResponse​

CreateWorkflowResponse contains the created workflow.

FieldTypeLabelDescription
workflowWorkflowworkflow 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.

FieldTypeLabelDescription
violationsDefinitionValidationError.FieldViolationrepeatedInnermost 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.

FieldTypeLabelDescription
instance_locationstringRFC 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).
messagestringHuman-readable schema violation text taken verbatim from the jsonschema leaf error — e.g. "must be <= 25" or "value must be one of &#34;agent_action&#34;, &#34;approval_gate&#34;, ...".

DeleteWorkflowRequest​

DeleteWorkflowRequest soft-deletes a workflow. #2002.

FieldTypeLabelDescription
workflow_idstringworkflow_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.

FieldTypeLabelDescription
run_idstringrun_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_keystringevent_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.
payloadbytespayload 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).

FieldTypeLabelDescription
runWorkflowRunrun 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.
deduplicatedbooldeduplicated 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.

FieldTypeLabelDescription
action_idstringaction_id is the workflow action UUID (required).
commentstringcomment is optional escalation comment.

EscalateActionResponse​

EscalateActionResponse contains the escalated action result.

FieldTypeLabelDescription
actionWorkflowActionaction is the updated workflow action.

GetNodeIORequest​

GetNodeIORequest retrieves the input/output payloads of one workflow_action.

FieldTypeLabelDescription
run_idstringrun_id is the parent run UUID (required, used for tenant scoping).
node_idstringnode_id is the workflow_action UUID (required).

GetNodeIOResponse​

GetNodeIOResponse returns the redacted input + output of a node.

FieldTypeLabelDescription
inputgoogle.protobuf.Structinput is the action's input JSON, with sensitive top-level keys stripped per the caller's clearance.
outputgoogle.protobuf.Structoutput 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_fieldsstringrepeatedredacted_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.

FieldTypeLabelDescription
run_idstringrun_id is the parent run UUID (required, used for run + tenant scoping).
node_idstringnode_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.

FieldTypeLabelDescription
eventsTranscriptEventrepeatedevents are ordered by (session_id, seq, created_at). Multiple attempts interleave by session_id — each event carries its own session_id.
redacted_fieldsstringrepeatedredacted_fields lists every content field removed across all events (stable, deduplicated). Empty when no redaction occurred (caller has full clearance).
session_missingboolsession_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).

FieldTypeLabelDescription
prompt_idstringprompt_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:

&#34;user_prompt&#34; — the LBE-B1 prompt. question / options[].value / allow_text /
text_placeholder / default_choice carry it; the answer goes to
WorkflowService.AnswerRunPrompt as {choice, text}.
&#34;question_gate&#34; — the WR2-5 structured question. `question` carries
question_spec.prompt; `options[].value` carries each option&#39;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.

FieldTypeLabelDescription
prompt_idstringprompt_id echoes the requested row id.
questionstringquestion is the human-facing prompt headline (from prompt_spec.question).
optionsRunPromptOptionrepeatedoptions 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_textboolallow_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_placeholderstringtext_placeholder is the display hint for the free-text field (display-only).
default_choicestringdefault_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_clearanceint32required_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_atgoogle.protobuf.Timestampexpires_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.

FieldTypeLabelDescription
toolsToolCatalogEntryrepeatedtools 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_observationsPollObservationrepeatedpoll_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.

FieldTypeLabelDescription
workflow_idstringworkflow_id is the workflow UUID (required).

GetWorkflowResponse​

GetWorkflowResponse contains the requested workflow.

FieldTypeLabelDescription
workflowWorkflowworkflow is the requested workflow.

GetWorkflowStatusRequest​

GetWorkflowStatusRequest retrieves workflow run status.

FieldTypeLabelDescription
run_idstringrun_id is the workflow run UUID (required).

GetWorkflowStatusResponse​

GetWorkflowStatusResponse contains workflow run status and actions.

FieldTypeLabelDescription
runWorkflowRunrun is the workflow run with status.
actionsWorkflowActionrepeatedactions are the workflow actions for this run.

ListPendingApprovalsRequest​

ListPendingApprovalsRequest lists actions awaiting approval.

FieldTypeLabelDescription
agent_idstringagent_id optionally filters by assigned agent.
limitint32limit controls pagination (default: 50, max: 100).
offsetint32offset controls pagination.

ListPendingApprovalsResponse​

ListPendingApprovalsResponse contains pending approvals with context.

FieldTypeLabelDescription
approvalsPendingApprovalrepeatedapprovals are the pending approval entries.
total_countint32total_count is the total number of pending approvals (for pagination).

ListRunsRequest​

ListRunsRequest pages through past runs of one workflow.

FieldTypeLabelDescription
workflow_idstringworkflow_id is the workflow whose runs to list (required).
page_sizeint32page_size caps the rows in the response. Default 20, max 100. Values outside (0, 100] are clamped silently.
page_tokenstringpage_token is the opaque cursor returned by a previous ListRuns call's next_page_token. Empty starts from the newest run.
status_filterRunStatusFilterstatus_filter narrows the result set; see RunStatusFilter.

ListRunsResponse​

ListRunsResponse returns one page of WorkflowRun rows.

FieldTypeLabelDescription
runsWorkflowRunrepeatedruns 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_tokenstringnext_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.

FieldTypeLabelDescription
page_sizeint32page_size caps the rows in the response. Default 20, max 100. Values outside (0, 100] are clamped silently.
page_tokenstringpage_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_filterRunStatusFilterstatus_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.

FieldTypeLabelDescription
runsVisibleRunRowrepeatedruns is the page of VisibleRunRow rows the caller may see.
next_page_tokenstringnext_page_token is the cursor to pass to the next ListVisibleRuns call. Empty means no more pages.

ListWorkflowsRequest​

ListWorkflowsRequest lists workflows with optional filtering.

FieldTypeLabelDescription
team_idstringteam_id optionally filters by team.
statusstringstatus optionally filters by workflow status.
limitint32limit controls pagination (default: 50, max: 100).
offsetint32offset controls pagination.

ListWorkflowsResponse​

ListWorkflowsResponse contains the workflow list.

FieldTypeLabelDescription
workflowsWorkflowrepeatedworkflows is the list of workflows.
total_countint32total_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.

FieldTypeLabelDescription
node_idstringnode_id is the step name the frame belongs to.
session_idstringsession_id is the attempt id (groups redos/laps; mirrors TranscriptEvent).
seqint64seq 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.
iterationint32iteration is the loop lap (mirrors NodeStatusUpdate.iteration); 0 outside a loop.
token_textstringtoken_text is a coalesced, security-filtered batch of LLM tokens. Suppressed (unset) for callers below clearance 4 (structural-frames-only floor).
tool_callNodeToolCalltool_call is a tool invocation surfaced live.
tool_resultNodeToolResulttool_result is a completed tool result surfaced live.
statusstringstatus is an agent status transition (thinking
user_promptUserPromptFrameuser_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.

FieldTypeLabelDescription
run_idstringrun_id is the parent run UUID.
node_idstringnode_id is the workflow_action UUID.
statusstringstatus is one of: queued
elapsed_msint64elapsed_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_usddoublecost_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_bystringblocked_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.

FieldTypeLabelDescription
tool_namestringtool_name is the invoked tool.
tool_call_idstringtool_call_id correlates the call with its later NodeToolResult.
args_summarystringargs_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.

FieldTypeLabelDescription
tool_call_idstringtool_call_id correlates the result with its NodeToolCall.
tool_namestringtool_name is the tool that produced the result.
summarystringsummary is a compact, clamped rendering of the result. Suppressed (empty) for callers below clearance 4 — the same free-text floor as token_text.
is_errorboolis_error is true when the tool returned an error.
duration_msint32duration_ms is the tool call's wall-clock duration.

PauseRunRequest​

PauseRunRequest pauses a workflow run at the next checkpoint.

FieldTypeLabelDescription
run_idstringrun_id is the workflow run UUID to pause (required).
reasonstringreason is an optional human-readable note recorded in the audit log.

PauseRunResponse​

PauseRunResponse returns the post-pause run state.

FieldTypeLabelDescription
runWorkflowRunrun 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.

FieldTypeLabelDescription
actionWorkflowActionaction is the workflow action awaiting approval.
workflow_namestringworkflow_name is the parent workflow name.
agent_namestringagent_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.

FieldTypeLabelDescription
toolstringtool is the observation provider namespace (e.g. "platform", "github").
actionstringaction is the observation-safe read (e.g. "read_run_state", "get_check_runs").
rationalestringrationale 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.

FieldTypeLabelDescription
action_idstringaction_id is the workflow action UUID (required).
commentstringcomment is optional rejection comment.

RejectActionResponse​

RejectActionResponse contains the rejected action result.

FieldTypeLabelDescription
actionWorkflowActionaction is the updated workflow action.

ResumeRunRequest​

ResumeRunRequest lifts a pause on a workflow run. WF-16 (#1819).

FieldTypeLabelDescription
run_idstringrun_id is the workflow run UUID to resume (required).
reasonstringreason 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).

FieldTypeLabelDescription
runWorkflowRunrun 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.

FieldTypeLabelDescription
run_idstringrun_id is the parent run UUID.
node_idstringnode_id is the workflow_action UUID the log line belongs to. Empty for run-level events (run started, run cancelled, run paused).
levelstringlevel is one of INFO
messagestringmessage is the human-readable log line.
tgoogle.protobuf.Timestampt 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.

FieldTypeLabelDescription
namestringname is the answer object's key (required) — what the client must use in the response_payload's fields object.
typestringtype is the declared field type: one of string
requiredboolrequired, when true, means the answer object MUST carry this field. A missing required field is rejected before the CAS with a renderable violation.
descriptionstringdescription 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.

FieldTypeLabelDescription
valuestringvalue is the machine token recorded as the answer's choice (required).
labelstringlabel is the display text for the option (optional).
descriptionstringdescription 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.

FieldTypeLabelDescription
workflow_idstringworkflow_id is the workflow UUID (required).
enabledboolenabled is the schedule-firing gate (W4). false leaves the workflow manual-only / dormant.
trigger_typestringtrigger_type is "manual" (default when empty) or "schedule".
trigger_configgoogle.protobuf.Structtrigger_config is the schedule cron/interval spec (unset for manual).

SetWorkflowEnabledResponse​

SetWorkflowEnabledResponse returns the workflow with its new trigger config and enabled state.

FieldTypeLabelDescription
workflowWorkflowworkflow 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.

FieldTypeLabelDescription
node_idstringnode_id is the step whose frames were dropped (empty when not node-specific).
from_seqint64from_seq is the last seq delivered before the gap (0 when unknown).
to_seqint64to_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.

FieldTypeLabelDescription
run_idstringrun_id is the workflow run UUID to stream (required).
node_idstringnode_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.

FieldTypeLabelDescription
snapshot_eventTranscriptEventsnapshot_event replays one already-captured transcript event (the verbatim GetNodeTranscript shape, clearance-redacted). Sent before snapshot_done.
liveNodeIOFramelive is a live frame coalesced from the running agent step (after snapshot_done). token_text is present only for clearance >= 4.
gapStreamGapgap 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_doneboolsnapshot_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.

FieldTypeLabelDescription
run_idstringrun_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.

FieldTypeLabelDescription
lineRunLogLineline is the log entry.

StreamRunStatusRequest​

StreamRunStatusRequest subscribes to live status transitions for one run.

FieldTypeLabelDescription
run_idstringrun_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).

FieldTypeLabelDescription
updateNodeStatusUpdateupdate 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).

FieldTypeLabelDescription
status_filterRunStatusFilterstatus_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.

FieldTypeLabelDescription
actionstringaction is the operation name (e.g. "get_issue").
descriptionstringdescription is a short human-readable label for the action.
argsToolCatalogArgrepeatedargs 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.

FieldTypeLabelDescription
namestringname is the with key (e.g. "repo", "number", "body").
typestringtype is the JSON value type: one of string
requiredboolrequired reports whether the action requires this argument.
descriptionstringdescription is a short human-readable label for the argument.
pinned_literalboolpinned_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.

FieldTypeLabelDescription
toolstringtool is the governed tool namespace (e.g. "github").
descriptionstringdescription is a short human-readable label for the tool.
actionsToolCatalogActionrepeatedactions 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.

FieldTypeLabelDescription
session_idstringsession_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.
seqint32seq 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_typestringevent_type is one of: assistant_message
contentgoogle.protobuf.Structcontent 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_atgoogle.protobuf.Timestampcreated_at is when the event was captured.

TriggerWorkflowRequest​

TriggerWorkflowRequest starts workflow execution.

FieldTypeLabelDescription
workflow_idstringworkflow_id is the workflow UUID to execute (required).
contextgoogle.protobuf.Structcontext is optional execution context data.

TriggerWorkflowResponse​

TriggerWorkflowResponse contains the started workflow run.

FieldTypeLabelDescription
runWorkflowRunrun is the created workflow run.

UpdateWorkflowDefinitionRequest​

UpdateWorkflowDefinitionRequest edits a workflow's stored definition. #2002.

FieldTypeLabelDescription
workflow_idstringworkflow_id is the workflow UUID (required).
definitiongoogle.protobuf.Structdefinition 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.

FieldTypeLabelDescription
workflowWorkflowworkflow is the updated workflow.

UpdateWorkflowStatusRequest​

UpdateWorkflowStatusRequest transitions a workflow's lifecycle status. WF-24 (#1888).

FieldTypeLabelDescription
workflow_idstringworkflow_id is the workflow UUID (required).
statusstringstatus is the target lifecycle status: draft

UpdateWorkflowStatusResponse​

UpdateWorkflowStatusResponse returns the workflow with its new status.

FieldTypeLabelDescription
workflowWorkflowworkflow 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.

FieldTypeLabelDescription
prompt_idstringprompt_id is the governance_approvals row id the answer is recorded against (== the node's blocked_by uuid). The client fetches detail via GetRunPrompt.
questionstringquestion is the prompt headline (already model-authored, not free-text tool output — surfaced at every clearance so the card is legible).
asked_by_stepstringasked_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_rolestringasked_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.

FieldTypeLabelDescription
run_idstringrun_id is the workflow run this event describes.
workflow_idstringworkflow_id is the run's parent workflow.
workflow_namestringworkflow_name is the parent workflow's display name (structural label).
statusstringstatus is the public RUN status: pending
nodes_totalint32nodes_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_doneint32nodes_done is the count of actions in a terminal status — the same set as WorkflowRun.nodes_done ('approved'
needs_inputboolneeds_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_approvalboolneeds_approval is true when the run is parked on an approval/question gate (a governance_approvals row with action_type='approval_gate'
asked_by_rolestringasked_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_idstringprompt_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.
leavingboolleaving 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_snapshotboolis_snapshot is true on the replay events emitted before snapshot_done; false on every live delta after it.
snapshot_doneboolsnapshot_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_namestringdisplay_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_kindRunPromptKindprompt_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.

FieldTypeLabelDescription
runWorkflowRunrun 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_namestringworkflow_name is the parent workflow's display name (workflows.name).
display_namestringdisplay_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_kindRunPromptKindprompt_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.

FieldTypeLabelDescription
idstringid is the workflow UUID.
org_idstringorg_id is the organization UUID.
team_idstringteam_id is the optional team UUID.
namestringname is the workflow name.
descriptionstringdescription is the workflow description.
definitiongoogle.protobuf.Structdefinition is the workflow definition JSON.
created_bystringcreated_by is the creator UUID.
statusstringstatus is the workflow status (draft, active, archived).
created_atgoogle.protobuf.Timestampcreated_at is the creation timestamp.
deleted_atgoogle.protobuf.Timestampdeleted_at is the optional deletion timestamp.
trigger_typestringtrigger_type is how the workflow is triggered: "manual" (default) or "schedule". Backed by workflows.trigger_type (migration 151, WF-23 #1887).
enabledboolenabled 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_statusstringlast_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_atgoogle.protobuf.Timestamplast_run_at is when this workflow's most recent run started. ROLLUP — WF-25 computes it; unset here.
run_countint32run_count is the total number of runs this workflow has had. ROLLUP — WF-25 computes it; 0 here.
avg_cost_usddoubleavg_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_atgoogle.protobuf.Timestampupdated_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.
visibilitystringvisibility 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.

FieldTypeLabelDescription
idstringid is the action UUID.
run_idstringrun_id is the parent run UUID.
org_idstringorg_id is the organization UUID.
step_namestringstep_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_idstringagent_id is the assigned agent UUID.
statusstringstatus 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.

FieldTypeLabelDescription
idstringid is the run UUID.
workflow_idstringworkflow_id is the parent workflow UUID.
org_idstringorg_id is the organization UUID.
triggered_bystringtriggered_by is the trigger user UUID.
statusstringstatus is the run status: pending
contextgoogle.protobuf.Structcontext is the execution context data.
started_atgoogle.protobuf.Timestampstarted_at is the execution start timestamp.
completed_atgoogle.protobuf.Timestampcompleted_at is the execution completion timestamp.
created_atgoogle.protobuf.Timestampcreated_at is the creation timestamp.
cost_usddoublecost_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_atgoogle.protobuf.Timestamppaused_at is set when the run was last paused via PauseRun (#1110); cleared on resume.
cancelled_atgoogle.protobuf.Timestampcancelled_at is set when the run was cancelled via CancelRun (#1110).
nodes_totalint32nodes_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_doneint32nodes_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.

NameNumberDescription
RUN_PROMPT_KIND_UNSPECIFIED0RUN_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_PROMPT1RUN_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_GATE2RUN_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_GATE3RUN_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).

NameNumberDescription
RUN_STATUS_FILTER_UNSPECIFIED0RUN_STATUS_FILTER_UNSPECIFIED returns runs in any status (== ALL).
RUN_STATUS_FILTER_ACTIVE1RUN_STATUS_FILTER_ACTIVE returns runs where status IN ('pending', 'running', 'paused').
RUN_STATUS_FILTER_COMPLETED2RUN_STATUS_FILTER_COMPLETED returns runs where status = 'completed'.
RUN_STATUS_FILTER_FAILED3RUN_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_ALL4RUN_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 NameRequest TypeResponse TypeDescription
CreateWorkflowCreateWorkflowRequestCreateWorkflowResponseCreateWorkflow creates a new workflow with definition validation.
GetWorkflowGetWorkflowRequestGetWorkflowResponseGetWorkflow retrieves a workflow by ID.
ListWorkflowsListWorkflowsRequestListWorkflowsResponseListWorkflows lists workflows for an organization with filtering.
TriggerWorkflowTriggerWorkflowRequestTriggerWorkflowResponseTriggerWorkflow starts execution of a workflow.
GetWorkflowStatusGetWorkflowStatusRequestGetWorkflowStatusResponseGetWorkflowStatus retrieves the current status of a workflow run.
ApproveActionApproveActionRequestApproveActionResponseApproveAction approves a workflow action awaiting approval.
RejectActionRejectActionRequestRejectActionResponseRejectAction rejects a workflow action awaiting approval.
EscalateActionEscalateActionRequestEscalateActionResponseEscalateAction escalates a workflow action to higher clearance.
ListPendingApprovalsListPendingApprovalsRequestListPendingApprovalsResponseListPendingApprovals lists workflow actions awaiting approval.
StreamRunStatusStreamRunStatusRequestStreamRunStatusResponse streamStreamRunStatus 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:

  1. 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 TypeNotesC++JavaPythonGoC#PHPRuby
doubledoubledoublefloatfloat64doublefloatFloat
floatfloatfloatfloatfloat32floatfloatFloat
int32Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead.int32intintint32intintegerBignum or Fixnum (as required)
int64Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead.int64longint/longint64longinteger/stringBignum
uint32Uses variable-length encoding.uint32intint/longuint32uintintegerBignum or Fixnum (as required)
uint64Uses variable-length encoding.uint64longint/longuint64ulonginteger/stringBignum or Fixnum (as required)
sint32Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s.int32intintint32intintegerBignum or Fixnum (as required)
sint64Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s.int64longint/longint64longinteger/stringBignum
fixed32Always four bytes. More efficient than uint32 if values are often greater than 2^28.uint32intintuint32uintintegerBignum or Fixnum (as required)
fixed64Always eight bytes. More efficient than uint64 if values are often greater than 2^56.uint64longint/longuint64ulonginteger/stringBignum
sfixed32Always four bytes.int32intintint32intintegerBignum or Fixnum (as required)
sfixed64Always eight bytes.int64longint/longint64longinteger/stringBignum
boolboolbooleanbooleanboolboolbooleanTrueClass/FalseClass
stringA string must always contain UTF-8 encoded or 7-bit ASCII text.stringStringstr/unicodestringstringstringString (UTF-8)
bytesMay contain any arbitrary sequence of bytes.stringByteStringstr[]byteByteStringstringString (ASCII-8BIT)