Streaming Workflows
Manage streaming workflows and their long-lived sessions (voice, telephony, WebRTC, WebSocket pipelines). Streaming workflows are the same workflows documents as batch — the workflowType: "streaming" filter selects them. Sessions hang off a deployed workflow (or an ephemeral TTL-bound deployment for dev runs) and expose their bidirectional frame stream over the WebSocket sub-resource documented at the end of this page.
All endpoints require authentication via X-API-Key header and the appropriate scope.
Streaming Workflow Object
{
"_id": "wf_abc123",
"name": "voice-intake",
"description": "Voice agent that triages inbound calls.",
"workflowType": "streaming",
"status": "deployed",
"ownerId": "user_456",
"organizationId": "org_xyz",
"sharedWith": [],
"isPublic": false,
"nodes": [ /* node graph */ ],
"edges": [ /* edges */ ],
"createdAt": "2026-04-10T09:00:00Z",
"updatedAt": "2026-05-12T14:22:00Z"
}
Streaming Session Object
{
"session_id": "sess_abc123",
"workflow_id": "wf_abc123",
"deployment_id": "dep_xyz",
"status": "active",
"ownerId": "user_456",
"organizationId": "org_xyz",
"metadata": { "callerId": "+15551234567" },
"correlationId": "corr_789",
"dispositionSchemaVersion": "v1",
"estimatedDurationSeconds": 600,
"started_at": "2026-05-16T09:30:00Z",
"ready_at": "2026-05-16T09:30:02Z",
"ended_at": null,
"ws_url": "wss://app.strongly.ai/api/v1/streaming/sessions/sess_abc123/ws",
"ws_token": "<short-lived JWT>"
}
GET /api/v1/streaming-workflows
List streaming workflows accessible to the caller (owned, shared, or public within the org).
Scope: streaming-workflows:read
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
search | string | No | Search by name or description |
status | string | No | Filter by workflow status |
limit | integer | No | Max results (default 50, max 200) |
offset | integer | No | Pagination offset |
sort | string | No | Sort field(s), -prefix for descending |
Response 200 OK
{
"count": 4,
"limit": 50,
"offset": 0,
"items": [ /* Streaming Workflow objects */ ]
}
GET /api/v1/streaming-workflows/:id
Get a single streaming workflow by ID.
Scope: streaming-workflows:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Response 200 OK
Returns the full Streaming Workflow object.
GET /api/v1/streaming-workflows/:id/deployments
List deployments for a streaming workflow, newest first.
Scope: streaming-workflows:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Response 200 OK
[
{
"_id": "dep_xyz",
"workflow_id": "wf_abc123",
"status": "ready",
"cpu": "1000m",
"memory": "2Gi",
"max_concurrent_sessions": 10,
"idle_timeout_seconds": 300,
"max_session_duration_seconds": 3600,
"created_at": "2026-05-12T14:22:00Z",
"updated_at": "2026-05-12T14:23:10Z"
}
]
GET /api/v1/streaming-workflows/:id/sessions
List streaming sessions for a workflow.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
status | string | No | Filter by session status. Pass active to match any non-terminal state (pending / scheduling / ready / connecting / active / draining). |
limit | integer | No | Max results |
offset | integer | No | Pagination offset |
sort | string | No | Sort field(s), -prefix for descending |
Response 200 OK
{
"count": 12,
"limit": 50,
"offset": 0,
"items": [ /* Streaming Session objects */ ]
}
POST /api/v1/streaming-workflows/:id/deploy
Deploy a streaming workflow. Routes through the unified workflows.deploy method, which branches on workflowType.
Scope: streaming-workflows:deploy
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Request Body
{
"cpu": "1000m",
"memory": "2Gi",
"disk": "20Gi",
"gpu": 0,
"gpu_type": null,
"idle_timeout_seconds": 300,
"max_session_duration_seconds": 3600,
"max_concurrent_sessions": 10
}
| Field | Type | Required | Description |
|---|---|---|---|
cpu | string | No | CPU request (Kubernetes form, e.g. 1000m) |
memory | string | No | Memory request (e.g. 2Gi) |
disk | string | No | Ephemeral disk request (e.g. 20Gi) |
gpu | integer | No | GPU count |
gpu_type | string | No | GPU type / accelerator class |
idle_timeout_seconds | integer | No | Idle-session shutdown threshold |
max_session_duration_seconds | integer | No | Hard cap on session length |
max_concurrent_sessions | integer | No | Per-deployment concurrency cap |
Response 201 Created
Returns the deployment record.
POST /api/v1/streaming-workflows/:id/undeploy
Undeploy a streaming workflow. Routes through the unified workflows.undeploy method.
Scope: streaming-workflows:deploy
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Workflow ID |
Response 200 OK
{ "workflowId": "wf_abc123", "status": "undeployed" }
POST /api/v1/streaming-sessions
Start a streaming session for a deployed streaming workflow. If the workflow has no deployment, the controller provisions an ephemeral TTL-bound deployment so dev "Run" works identically to batch.
Scope: streaming-sessions:write
Request Body
{
"workflowId": "wf_abc123",
"metadata": { "callerId": "+15551234567" },
"dispositionSchemaVersion": "v1",
"estimatedDurationSeconds": 600,
"correlationId": "corr_789"
}
| Field | Type | Required | Description |
|---|---|---|---|
workflowId | string | Yes | Workflow to run |
metadata | object | No | Free-form metadata attached to the session |
dispositionSchemaVersion | string | No | Disposition schema version the caller expects |
estimatedDurationSeconds | number | No | Estimated session duration (used for the budget gate) |
correlationId | string | No | Correlation ID for tracing |
Response 201 Created
{
"session_id": "sess_abc123",
"ws_url": "wss://app.strongly.ai/api/v1/streaming/sessions/sess_abc123/ws",
"ws_token": "<short-lived JWT>"
}
The ws_token is a short-lived JWT minted by the controller. Use it to authenticate the WebSocket upgrade described in WebSocket — /api/v1/streaming/sessions/:sessionId/ws.
GET /api/v1/streaming-sessions
List streaming sessions accessible to the caller.
Scope: streaming-sessions:read
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
workflowId | string | No | Filter by workflow ID |
status | string | No | Filter by status. Pass active to match any non-terminal state. |
limit | integer | No | Max results |
offset | integer | No | Pagination offset |
sort | string | No | Sort field(s), -prefix for descending |
Response 200 OK
{
"count": 5,
"limit": 50,
"offset": 0,
"items": [ /* Streaming Session objects */ ]
}
GET /api/v1/streaming-sessions/:id
Get a single streaming session.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Response 200 OK
Returns the full Streaming Session object. Returns 404 not-found for sessions outside the caller's tenant (cross-tenant existence is masked per ADR-S2).
GET /api/v1/streaming-sessions/:id/handoffs
List multi-agent handoff history for a session (read-only mirror of streaming_handoffs). Tenant-scoped: the caller must have access to the underlying session.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Response 200 OK
{
"handoffs": [
{
"_id": "ho_abc123",
"session_id": "sess_abc123",
"from_agent": "triage",
"to_agent": "billing",
"reason": "billing question",
"started_at": "2026-05-16T09:31:00Z",
"completed_at": "2026-05-16T09:31:02Z"
}
]
}
DELETE /api/v1/streaming-sessions/:id
End a streaming session.
Scope: streaming-sessions:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Response 204 No Content
POST /api/v1/streaming-sessions/:id/inject
Inject a text message into a running session — useful for surfacing operator messages, simulated user turns, or out-of-band instructions on top of the live audio/video stream.
Scope: streaming-sessions:write
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Request Body
{
"text": "Please ask the caller for their account number.",
"role": "system"
}
| Field | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Message body to inject |
role | string | No | Role to attribute the message to (e.g. system, user, assistant) |
Response 200 OK
{ "injected": true }
GET /api/v1/streaming-sessions/:id/transcript
Paginated transcript for a session, sorted oldest-first.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Query Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Max results |
offset | integer | No | Pagination offset |
Response 200 OK
{
"count": 38,
"limit": 50,
"offset": 0,
"items": [
{
"_id": "txn_abc",
"session_id": "sess_abc123",
"role": "user",
"text": "I'd like to check my balance.",
"emitted_at": "2026-05-16T09:30:05Z"
}
]
}
GET /api/v1/streaming-sessions/:id/metrics
Session metric windows, sorted oldest-first.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Response 200 OK
[
{
"_id": "met_abc",
"session_id": "sess_abc123",
"window_start": "2026-05-16T09:30:00Z",
"window_end": "2026-05-16T09:30:10Z",
"asr_words": 18,
"tts_chars": 64,
"barge_in_count": 0
}
]
GET /api/v1/streaming-sessions/:id/errors
List streaming errors recorded for a session, oldest-first.
Scope: streaming-sessions:read
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Session ID |
Response 200 OK
[
{
"_id": "err_abc",
"session_id": "sess_abc123",
"code": "stt.timeout",
"message": "STT call exceeded watchdog",
"node_id": "stt-1",
"timestamp": "2026-05-16T09:30:08Z"
}
]
WebSocket — /api/v1/streaming/sessions/:sessionId/ws
The session is the resource; /ws is the sub-resource that initiates a WebSocket upgrade. Meteor is the only public edge — external clients never reach the cluster-internal streaming-gateway or the pod directly. The proxy authenticates the upgrade, then pipes frames bidirectionally with no transcoding.
Path Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
sessionId | string | Yes | Session ID (UUID-shaped: [A-Za-z0-9_-]{1,128}) |
Authentication
Pass the ws_token returned by POST /api/v1/streaming-sessions in one of:
- Header (programmatic clients):
Authorization: Bearer <ws_token> - Query string (browser
WebSocketconstructor, which cannot set headers):wss://app.strongly.ai/api/v1/streaming/sessions/<sessionId>/ws?ws_token=<ws_token>
The ws_token is a short-lived JWT signed with the platform JWT secret. The gateway validates it on connect.
Resume Cursor
After a disconnect, clients may pass ?resume_from=<watermark> to ask the upstream pod to replay buffered JSON frames from that watermark. The cursor must be a 1–19 digit numeric string; malformed values are ignored.
Frame Protocol
The connection carries raw frames in both directions with no transcoding at the proxy. The Strongly streaming frame schema (audio, transcript, tool-call, error, disposition, etc.) is defined alongside the pipeline node catalog; see the streaming pipeline reference for the full frame roster. Application-level keepalive: the proxy sends WS pings every 15s to keep the upgrade alive under L4 connection tracking.
Close Codes
| Code | Meaning | Client Behavior |
|---|---|---|
1000 | Normal closure (session ended cleanly) | Done. |
1011 | Proxy error (e.g. upstream connect failure after retry budget exhausted) | Do not retry on this connection. |
1012 | Session was reassigned to a different pod | Reconnect — retryable. |
1013 | Gateway/pod at capacity or cold-start in progress | Reconnect with exponential backoff — retryable. |
The proxy itself transparently retries upstream 1012 and 1013 closes during the initial connect budget; clients only see them if they occur after the upgrade is live.
Pre-Upgrade Failure Responses
The upgrade may fail before the WebSocket is established. These come back as plain HTTP responses:
| Status | Meaning |
|---|---|
401 Unauthorized | Missing or malformed ws_token |
404 Not Found | Unknown session, or session is in a terminal state (only pending / scheduling / ready / connecting / active are proxyable) |
500 Internal Server Error | Auth lookup failed unexpectedly |