Conductor Protocol (WebSocket)
On this page
- Connection
- Handshake Sequence
- Envelope
- Message Catalogue
- Core Lifecycle and Execution
- Workflow Inspection and Queries
- Workflow Forking and Portability
- Schedules
- Queues and Metrics
- Applications and Alerts
- Unimplemented Types
- Liveness, Heartbeat, and Timeout
- Recovery Semantics
- Lifecycle State Machine
- Timing and Grace Periods
- Recovery Failover and Peer Selection
- Idempotence Guarantees
- Alert Notifications
- High Availability and Peer Forwarding
- SDK Differences Matrix
- Version Skew Policy
The Executor WebSocket protocol defines full-duplex communication between Relay and registered DBOS Conductor executors.
Connection
The Conductor WebSocket URL is constructed by stripping any trailing slashes from the base URL and appending /websocket/{appName}/{conductorKey}:
wss://<relay-host>/websocket/{appName}/{conductorKey}
The API key is transmitted in the URL path ({conductorKey}).
Relay derives this protocol from the following public DBOS Transact SDK repositories:
- Python SDK:
dbos-transact-py(commit833794f7a1138bacf75ff6d88647a33eb5e35e52) - TypeScript SDK:
dbos-transact-ts(commitd8c4974cca6cc84b296f3b8edfbbb41627ddd47e) - Go SDK:
dbos-transact-go(commitab56911fdd78552e1e7fe648cff7c831a1e760c8) - Java SDK:
dbos-transact-java(commit1248174f393bd97f9973ec83cbc6e42b6e319ed1)
Handshake Sequence
Upon WebSocket connection establishment, Relay initiates the handshake by sending an executor_info request frame to the executor (conductor_protocol.go:82-95):
{
"type": "executor_info",
"request_id": "req-init-1"
}
The connecting executor must reply with an executor_info response frame containing registration metadata:
{
"type": "executor_info",
"request_id": "req-init-1",
"executor_id": "exec-uuid",
"application_version": "1.0.0",
"hostname": "worker-1",
"language": "go",
"dbos_version": "0.1.0",
"executor_metadata": {}
}
executor_id: Unique identifier for the executor instance.application_version: Application release version deployed on the executor.hostname: Host running the executor process.language: Runtime language (go,typescript,python,java).dbos_version: SDK version string.executor_metadata: Optional key-value metadata map.
Envelope
All wire messages share a common JSON envelope (internal/protocol/envelope.go:41-46):
type(string): The message type discriminator.request_id(string): Unique identifier correlating requests with responses.error_message(string, optional): Present only in response messages when an operation fails.
errorand payload are not valid wire envelope fields.
Message Catalogue
Relay supports 32 distinct protocol message types, matching the upstream Go SDK (dbos-transact-go/dbos/conductor_protocol.go commit ab56911fdd78552e1e7fe648cff7c831a1e760c8).
Core Lifecycle and Execution
-
executor_info- Request: Envelope only.
- Response:
executor_id(string),application_version(string),hostname(string, optional),language(string),dbos_version(string),executor_metadata(object, optional).
-
recovery- Request:
executor_ids(array of strings). Dispatched to instruct an active executor to recover workflows previously assigned to dead executor instances. - Response:
success(boolean).
- Request:
-
cancel- Request:
workflow_id(string, optional),workflow_ids(array of strings, optional),cancel_children(boolean). - Response:
success(boolean).
- Request:
-
resume- Request:
workflow_id(string, optional),workflow_ids(array of strings, optional),queue_name(string, optional). - Response:
success(boolean).
- Request:
-
delete- Request:
workflow_id(string, optional),workflow_ids(array of strings, optional),delete_children(boolean). - Response:
success(boolean).
- Request:
-
exist_pending_workflows- Request:
executor_id(string),application_version(string). - Response:
exist(boolean).
- Request:
-
retention- Request:
body(object):gc_cutoff_epoch_ms(integer, optional),gc_rows_threshold(integer, optional),gc_batch_size(integer, optional),timeout_cutoff_epoch_ms(integer, optional). - Response:
success(boolean).
- Request:
Workflow Inspection and Queries
-
list_workflows- Request body:
workflow_uuids(array of strings, optional),workflow_name(string or array, optional),authenticated_user(string or array, optional),start_time(timestamp, optional),end_time(timestamp, optional),status(string or array, optional),application_version(string or array, optional),limit(integer, optional),offset(integer, optional),sort_desc(boolean),load_input(boolean),load_output(boolean),queues_only(boolean). - Response:
output(array of workflow status records).
- Request body:
-
list_queued_workflows- Request body: Same query fields as
list_workflows, withqueue_namefilter. - Response:
output(array of workflow status records).
- Request body: Same query fields as
-
get_workflow- Request:
workflow_id(string),load_input(boolean),load_output(boolean). - Response:
output(single workflow status record).
- Request:
-
list_steps- Request:
workflow_id(string),load_output(boolean),limit(integer, optional),offset(integer, optional). - Response:
output(array of step records).
- Request:
-
get_workflow_events- Request:
workflow_id(string). - Response:
events(array of{key, value}).
- Request:
-
get_workflow_notifications- Request:
workflow_id(string). - Response:
notifications(array of{topic, message, created_at_epoch_ms, consumed}).
- Request:
-
get_workflow_streams- Request:
workflow_id(string). - Response:
streams(array of{key, values}).
- Request:
-
get_workflow_aggregates- Request body: Aggregation filter parameters.
- Response:
output(array of aggregate rows).
-
get_step_aggregates- Request body: Step aggregation filter parameters.
- Response:
output(array of step aggregate rows).
Workflow Forking and Portability
-
fork_workflow- Request body:
workflow_id(string),start_step(integer),new_workflow_id(string, optional),application_version(string, optional),queue_name(string, optional),queue_partition_key(string, optional). - Response:
new_workflow_id(string, optional).
- Request body:
-
fork_from_failure- Request body:
workflow_ids(array of strings),application_version(string, optional),queue_name(string, optional),queue_partition_key(string, optional),from_last_failure(boolean, optional),from_last_step(boolean, optional),from_step(integer, optional),from_step_name(string, optional). - Response:
forked_workflow_ids(array of strings).
- Request body:
-
export_workflow- Request:
workflow_id(string),export_children(boolean). - Response:
serialized_workflow(string, optional).
- Request:
-
import_workflow- Request:
serialized_workflow(string). - Response:
success(boolean).
- Request:
Schedules
-
list_schedules- Request:
body(object, optional filter parameters:status,workflow_name,schedule_name_prefix,application_name,load_context). - Response:
output(array of schedule objects).
- Request:
-
get_schedule- Request:
schedule_name(string),load_context(boolean, optional). - Response:
output(schedule object).
- Request:
-
pause_schedule- Request:
schedule_name(string). - Response:
success(boolean).
- Request:
-
resume_schedule- Request:
schedule_name(string). - Response:
success(boolean).
- Request:
-
trigger_schedule- Request:
schedule_name(string). - Response:
workflow_id(string, optional).
- Request:
-
backfill_schedule- Request:
schedule_name(string),start(timestamp string, ISO 8601),end(timestamp string, ISO 8601). - Response:
workflow_ids(array of strings).
- Request:
Queues and Metrics
-
list_queues- Request:
body(object, optional filter parameterapplication_name). - Response:
output(array of queue metadata objects).
- Request:
-
get_queue- Request:
name(string). - Response:
output(queue metadata object).
- Request:
-
get_metrics- Request:
start_time(timestamp string, RFC 3339),end_time(timestamp string, RFC 3339),metric_class(string),application_name(array of strings, optional). - Response:
metrics(array of{metric_name, metric_type, value}).
- Request:
Applications and Alerts
-
list_application_versions- Request: Envelope only.
- Response:
output(array of application version objects with{version_id, version_name, version_timestamp, created_at}).
-
set_latest_application_version- Request:
version_name(string). - Response:
success(boolean).
- Request:
-
alert- Request: Dispatched by Relay to connected executors with
{name, message, metadata}(conductor_protocol.go:590-595). - Response:
success(boolean).
- Request: Dispatched by Relay to connected executors with
Unimplemented Types
restart: Present in Python (protocol.py:37) and Java SDKs, but not implemented in Relay or the Go SDK.
Liveness, Heartbeat, and Timeout
Relay and connected executors exchange heartbeats to maintain active connection health:
- Server ping interval: 10 seconds.
- Client ping interval: 10 seconds (or SDK default of 20 seconds).
- Pong deadline: 25 seconds (
executorPingWait). - Reconnect backoff: Executors apply exponential backoff between 1 second and SDK-defined ceilings on disconnect.
Recovery Semantics
Relay monitors executor connectivity and orchestrates workflow recovery across peer instances.
Lifecycle State Machine
Each executor registration transitions through four discrete states:
- Connected (
HEALTHY): Active WebSocket connection responding to heartbeat frames. - Disconnected (
DISCONNECTED): Socket connection dropped or timed out. A per-application grace timer begins. If the executor reconnects with matchingexecutor_idbefore expiry, it returns toHEALTHY. - Dead (
DEAD): The grace period expired without reconnection. Relay marks the instance dead and initiates workflow recovery dispatch. - Deleted: Once recovery is acknowledged by an active peer, the dead executor record is pruned from the registry.
Timing and Grace Periods
- Default grace period: 60 seconds (cited from DBOS public documentation
/production/workflow-recovery). - Application override: Configurable per-application via the
executorTimeoutSecssetting in application metadata (Conductor OpenAPIApplicationandPatchAppInputBody). - Server ping interval: 10 seconds.
Recovery Failover and Peer Selection
When an executor transitions to DEAD:
- Relay queries connected healthy peers within the same application.
- Cross-application or cross-organisation recovery is rejected.
- Candidates with a matching
application_versionare prioritized to prevent workflow version skew. - Relay dispatches a
recoverymessage withexecutor_ids: [dead_executor_id]to the selected peer. - If the chosen peer disconnects or fails to acknowledge within the dispatch deadline, Relay fails over sequentially to the next healthy candidate.
- Upon receiving a response with
success: true, Relay deletes the dead executor record from the registry. - If no healthy peers are currently connected, the dead record remains in
DEADstatus until a peer connects.
Idempotence Guarantees
Relay inherits the execution guarantees of the upstream SDK implementation and system database:
- Step executions are at-least-once.
- Workflow outcomes are exactly-once.
- Recovery dispatch is idempotent. Multiple recovery dispatches for the same dead executor do not corrupt execution state because recovery re-enqueue in the SDK system database (
dbos-transact-godbos/internal/sysdb/system_database.go:5098ReenqueueForRecovery) is scoped tostatus = PENDING. Rows previously transitioned toENQUEUEDby an initial dispatch are unaffected by duplicate dispatches.
Alert Notifications
Relay evaluates alerting rules against application metrics and dispatches an alert frame to registered executors:
{
"type": "alert",
"request_id": "alert-uuid",
"name": "high_cpu_utilization",
"message": "CPU usage exceeded threshold",
"metadata": {
"metric_name": "cpu_utilization",
"threshold": "90"
}
}
Receiving executors dispatch alerts to registered handlers (@DBOS.alert_handler in Python, DBOS.setAlertHandler in TypeScript, and conductor protocol handler in Go).
High Availability and Peer Forwarding
In multi-instance deployments, Relay instances coordinate state through the shared control plane database:
- Instance Registration: Each Relay instance registers its unique ID, advertise address, and port in the
instancestable and maintains a periodic heartbeat. - Executor Lease Ownership: Executors connecting via WebSocket are assigned to the receiving Relay instance with a lease. Surviving instances adopt expired leases upon heartbeat timeout.
- Cross-Instance Peer Forwarding: When an API request targets an application whose connected executors reside on a peer Relay instance, the receiving instance signs the payload using HMAC-SHA256 and forwards it over HTTP to
/internal/v1/forward/{appID}on the target peer. - Loop Prevention and Drift Check: Forwarded requests carry an
X-Relay-Forward-Hopheader. Requests withhop >= 1are rejected with HTTP 409 Conflict to prevent forwarding loops. Forward signatures include Unix timestamps with a maximum allowed drift of 30 seconds.
SDK Differences Matrix
| Feature | TypeScript | Python | Go | Java |
|---|---|---|---|---|
metadata |
Supported | Supported | Supported | Supported |
cancel_children |
Supported | Supported | Supported (conductor_protocol.go:462-467) |
Supported (CancelRequest.java:8) |
Version Skew Policy
- SDK clients log unknown message types and reply with structured error responses (
dbos-transact-go/dbos/conductor.go:433,dbos-transact-ts/src/conductor/conductor.ts:921,Conductor.java:255,conductor.py:1168). - Standard JSON unmarshalling in SDKs ignores unknown fields on typed unmarshal.