Skip to content

Streaming Events

Fabric provides real-time event streaming for all domain events — workflow execution, tenancy changes, and provider operations.

import { FabricClient } from "@fabric-platform/sdk";
const fabric = new FabricClient({ apiKey: "fab_xxx" });
await fabric.streamEvents((event) => {
console.log(event.kind, event.node_key, event.payload);
});

Events arrive as SSE with this format:

event: workflow.run.started
data: {"id":"...","kind":"workflow_run_started","run_id":"...","payload":{}}
event: workflow.node.completed
data: {"id":"...","kind":"workflow_node_completed","node_key":"generate","payload":{"output":{}}}
const result = await fabric.waitForRun("<run-id>", {
onEvent: (event) => {
console.log(`Node ${event.node_key}: ${event.kind}`);
},
});

This endpoint:

  1. Replays all existing events for the run (catch-up)
  2. Streams live events as they occur (filtered to this run)
  3. Auto-closes after a terminal event (workflow.run.completed, workflow.run.failed, workflow.run.cancelled)

A client connecting mid-execution receives the full event history before seeing live updates.

Each SSE message includes an id: field (the event UUID). If the connection drops, pass the last received ID via the Last-Event-ID header to skip already-seen events on reconnect:

Terminal window
curl -N -H "Last-Event-ID: evt-uuid-of-last-seen" \
https://gofabric.dev/v1/workflow-runs/{run_id}/events

The TypeScript SDK handles this automatically when reconnect: true is set.

Fabric’s workflow SDK wraps every workflow in internal shim tasks (_fabric_capture_input, _fabric_finalize_output). These are hidden from event streams by default. To include them (useful for debugging), add ?include_internal=true:

Terminal window
curl -N "https://gofabric.dev/v1/workflow-runs/{run_id}/events?include_internal=true"

The TypeScript SDK’s SSE layer uses fetch + ReadableStream, not the browser’s native EventSource. This means fabric.workflows.runs.watch() and streamEvents() work directly from the browser with custom Authorization headers — no server-side proxy required.

Auth pattern: never ship a long-lived fab_... API key to the browser. Configure the client with a dynamic token provider that fetches a short-lived token from your own backend:

import { FabricClient } from "@fabric-platform/sdk";
export const fabric = new FabricClient({
baseUrl: "https://api.fabric.example.com",
auth: async () => {
const res = await fetch("/api/fabric-token", { credentials: "include" });
const { token } = await res.json();
return token;
},
});

The provider is re-invoked on every fetch call, so tokens refresh naturally on reconnect. Your backend only needs one route: mint a short-lived Fabric token for the logged-in user.

React usage — the SDK ships a useWorkflowRun hook at @fabric-platform/sdk/react that wraps watch() and gives you typed state:

import { useState } from "react";
import { useWorkflowRun } from "@fabric-platform/sdk/react";
import { fabric } from "./fabric-client";
export function RunTrigger({ workflowName }: { workflowName: string }) {
const [runId, setRunId] = useState<string | null>(null);
const run = useWorkflowRun(fabric, runId);
return (
<div>
<button
disabled={runId !== null && !run.isComplete}
onClick={async () => {
const r = await fabric.workflows.runs.submit(workflowName, {
input: { topic: "quarterly trends" },
});
setRunId(r.id);
}}
>
Run
</button>
<progress value={run.progressPercent} max={100} />
<p>{run.currentStep ?? (runId ? "waiting…" : "idle")}</p>
{run.error && <p>Error: {run.error.message}</p>}
{run.isComplete && <p>Done: {run.runStatus}</p>}
</div>
);
}

CORS requirement: your Fabric API must include the caller’s origin in CORS_ALLOWED_ORIGINS, and the allow_headers list must cover Authorization, Cache-Control, and Last-Event-ID. The default tower_http::cors config that ships with fabric serve already includes all three.

When to use what:

ApproachTransportAuthUse when
useWorkflowRunfetch + ReadableStreamBearer token (dynamic provider)You want direct browser → Fabric streaming, no proxy hop
usePipelineProgressNative EventSourceCookies / same-origin onlyYou already proxy SSE through your own server via createSSEResponse()

Both are legitimate. useWorkflowRun is strictly newer; reach for it when starting fresh.

await fabric.streamEvents("<job-id>", (event) => {
console.log(event.kind, event.payload);
});

Same replay + live streaming behavior, filtered to the specific job.

For programmatic consumers that prefer line-by-line parsing without SSE framing, use the NDJSON endpoint:

Terminal window
curl -N https://gofabric.dev/v1/workflow-runs/{run_id}/events/ndjson

Each line is a standalone JSON object:

{"id":"...","kind":"workflow_run_started","run_id":"...","payload":{}}
{"id":"...","kind":"workflow_node_completed","node_key":"generate","payload":{"output":{}}}
{"id":"...","kind":"workflow_run_completed","payload":{"output":{...}}}

Same replay + live + auto-close behavior as SSE, just without event type headers or id: lines.

Terminal window
wscat -c wss://gofabric.dev/v1/events/ws

WebSocket delivers the same event structure as SSE, using JSON messages.

Connect directly to a run-scoped WebSocket:

Terminal window
wscat -c wss://gofabric.dev/v1/workflow-runs/{run_id}/ws

WebSocket connections support bidirectional communication. Send JSON commands to control the stream:

{"subscribe_run": "run-uuid"}
{"unsubscribe": true}
{"include_internal": true}
CommandDescription
subscribe_runFilter events to a specific run ID
unsubscribeClear the run filter, receive all events again
include_internalToggle visibility of internal SDK shim tasks

Unlike SSE, WebSocket streams do not replay historical events — new clients start from the current moment. The connection persists until the client disconnects (no auto-close on terminal events).

The GraphQL API exposes event subscriptions over WebSocket:

subscription {
workflowRunEvents(runId: "run-uuid") {
id
kind
kindRaw
organizationId
runId
nodeKey
payload
timestamp
}
}

Use events(orgId: "org-uuid") to subscribe to all events for an organization. Maximum 10 concurrent subscriptions per principal.

EventDescription
workflow.run.createdRun was created
workflow.run.startedExecutor began processing
workflow.run.completedAll nodes completed successfully
workflow.run.failedOne or more nodes failed
workflow.run.cancelledRun was cancelled
workflow.node.readyNode dependencies satisfied
workflow.node.startedNode execution began
workflow.node.completedNode finished with output
workflow.node.failedNode execution failed
workflow.node.skippedNode skipped (dependency failed or cancelled)
EventDescription
job.createdJob was created
job.startedExecutor began processing
job.completedJob finished with output
job.failedJob execution failed
EventDescription
organization.createdOrganization was created
team.createdTeam was created
membership.createdMembership was created
invitation.createdInvitation was sent
invitation.acceptedInvitation was accepted
invitation.revokedInvitation was revoked

Every event follows this structure:

{
"id": "event-uuid",
"kind": "workflow.node.completed",
"organization_id": "org-uuid or null",
"workflow_id": "wf-uuid or null",
"run_id": "run-uuid or null",
"node_key": "generate or null",
"payload": { "output": {}, "context_version": 3 },
"timestamp": "2026-03-19T12:00:00Z"
}

If a workflow run was submitted with an opaque state object, that state is echoed back inside payload.state on terminal events (workflow.run.completed, workflow.run.failed, workflow.run.cancelled). This lets consumers correlate completions with caller-supplied context without a follow-up API call. See the Webhooks Reference for details.

Terminal events include an output_url field in the payload — a stable API URL that returns the workflow output and all artifacts with signed download URLs. See the Webhooks Reference for the full response shape and SDK examples.

{
"kind": "workflow.run.completed",
"payload": {
"output_url": "https://gofabric.dev/v1/workflows/runs/run-uuid/output"
}
}

Workflow-generated assets are automatically cleaned up after 72 hours. Download or mirror assets promptly. See Asset Lifecycle for details.

Fabric supports webhook delivery for push-based event notifications. Register an HTTP endpoint, and Fabric will POST signed payloads for every matching event with automatic retries.

  • HMAC signing — Every delivery is signed with HMAC-SHA256 (X-Fabric-Signature header)
  • Exponential backoff — Automatic retries with configurable max_retries (30s × 2attempt)
  • Event & resource filtering — Subscribe to specific event types and scope to individual workflows or runs
  • Delivery tracking — Full delivery history with status, response codes, and error messages

All transports (SSE, WebSocket, webhooks) deliver the same DomainEvent structure.

See the Webhooks Reference for the complete guide — event catalog, SDK examples, signature verification, and API endpoints.

SSENDJSONWebSocketGraphQL SubWebhooks
ProtocolHTTP long-livedHTTP long-livedWSWS (GraphQL)HTTP POST
Replays historyYesYesNoNoNo
Auto-closes on terminalYesYesNoNoN/A
ReconnectionLast-Event-IDManualManualProtocol-levelAutomatic retries
DirectionServer → ClientServer → ClientBidirectionalServer → ClientServer → Server
Best forBrowser UIs, progress barsCLI tools, scriptsInteractive appsGraphQL clientsBackend pipelines, automation
  • Browser UI showing run progress: SSE via useWorkflowRun hook — replay ensures late-connecting clients see full history, auto-close cleans up on completion.
  • Backend processing on completion: Webhooks — reliable delivery with retries, HMAC signing, and no long-lived connections.
  • CLI or scripting: NDJSON — simple line-by-line parsing, no SSE framing overhead.
  • Interactive dashboards: WebSocket — bidirectional commands let you subscribe/unsubscribe dynamically.
  • GraphQL clients: GraphQL subscriptions — native integration with your existing GraphQL transport.