Skip to main content

Real-time Events over the Socket Gateway

ExecutionMode.SOCKET runs a task asynchronously but delivers its events pushed over the socket gateway instead of polled. It's the low-latency variant of async: progress, partial results, and custom events arrive as the worker emits them, and run() resolves on the terminal event.

What it adds over async polling​

Async (ExecutionMode.ASYNC) polls the result endpoint on an interval. SOCKET mode delivers the same events, but:

  • Lower latency — events arrive as they're produced, not on the next poll tick.
  • Less load — no repeated GET .../result requests while the task runs.

A brief disconnect is handled for you — the gateway restores your subscription and replays the events you missed (see Reconnection). A disconnect longer than the recovery window can't be recovered, and the call ends with an error so it never hangs on a stream that's gone. If you need a run to survive longer outages, ASYNC polling is the more forgiving choice.

Prerequisites: give the client a socket​

The task events are user-scoped, so the client needs a socket connection authenticated as the user. Two tokens are involved, and they are different:

TokenWherePurpose
App token (paat-…)identityToken (HTTP)Authorizes the workflow submit/result calls.
User OAuth token (JWT)socket handshake auth.tokenAuthenticates the socket to the gateway.

You can either bring your own socket or let the client create and manage one:

import { WorkflowsClient } from '@picsart/workflows-client';
import { io } from 'socket.io-client';

// Option A — bring your own socket.io socket (you own its lifecycle):
const socket = io('https://api.picsart.com', {
path: '/socket-gateway',
transports: ['websocket'],
auth: { token: `Bearer ${userOAuthToken}` }, // user JWT, not the app token
});
const workflows = new WorkflowsClient({ identityToken, socket });

// Option B — let the client create + own the socket (needs socket.io-client installed):
const workflows = new WorkflowsClient({
identityToken,
baseUrl: 'https://api.picsart.com/',
socketConnection: {
getToken: () => userOAuthToken,
// url defaults to the client's baseUrl; path to '/socket-gateway', transports to ['websocket']
},
});

socket.io-client is an optional peer dependency — it is imported on demand only when you use socketConnection, so consumers that don't use sockets never load it.

When a socket/socketConnection is configured, the client opens the connection eagerly at construction, so it's ready before your first run/subscribe (no connect latency on first use).

Running a task with pushed events​

import { ExecutionMode, WorkflowEvent } from '@picsart/workflows-client';

const { result, usage, status } = await workflows.run<MyResponseType>('example-task', params, {
mode: ExecutionMode.SOCKET,
onAccepted: (taskId) => console.log('submitted:', taskId),
onProgress: (p) => console.log('progress:', p), // { percent, estimatedSecondsLeft? }
onPartialResult: (pr) => console.log('partial:', pr), // latest partial snapshot
onEvent: (e: WorkflowEvent) => console.log('event:', e), // custom flow.emitEvent() events
});

mode: ExecutionMode.SOCKET selects socket delivery. Like the other modes, a remote-settings executionMode (if one is configured for this workflow) overrides the mode you pass — so a workflow pinned to ASYNC/SYNC/STREAM in remote settings won't switch to socket delivery (see Settings Integration). The mode requires a socket or socketConnection — without one, run() rejects with a WorkflowsError (400, reason: 'invalid_state').

The callbacks match the other modes: onProgress for metrics, onPartialResult for partial result snapshots, and onEvent for custom events (the event. prefix is stripped from event.type). run() resolves with { result, usage, status } on completion and rejects with a WorkflowsError (reason: 'workflow_failed') on failure.

Watching an existing task​

run submits and runs a task. subscribe watches work that's already running — a task submitted elsewhere, or before a page reload. It's shaped for observation rather than running: an async iterable you consume with for await, where each item is the raw gateway message { taskId, workflow?, type, payload }.

import { EventTypes } from '@picsart/workflows-client';

for await (const msg of workflows.subscribe({ name: 'example-task', taskId })) {
switch (msg.type) {
case EventTypes.METRICS:
console.log('progress', msg.payload);
break;
case EventTypes.PARTIAL_RESULT:
console.log('partial', msg.payload);
break;
case EventTypes.COMPLETED:
console.log('done', msg.payload.result, msg.payload.usage);
break;
case EventTypes.FAILED:
console.log('failed', msg.payload);
break;
default:
console.log('custom event', msg.type, msg.payload); // 'event.<name>'
}
}
  • Scope — with a taskId the loop ends after that task's COMPLETED/FAILED; without one it watches every task of the workflow and keeps running (one task finishing doesn't stop it).
  • Stopping — break the loop, or pass signal: anAbortSignal and abort it.
  • Errors — an unrecoverable socket session makes the for await throw (try/catch to re-subscribe); a task failing is just a FAILED message, not a throw.
  • Custom-event names keep the event. prefix (it's what separates them from lifecycle events in the single stream) — use msg.type.replace(/^event\./, '') for the bare name. run's onEvent strips it for you.

To poll /result instead of using the socket, use runPolling, or result for a one-off check.

Closing the socket​

await workflows.disconnect();

disconnect() closes a socket the client created from socketConnection. If you injected your own socket, it is left untouched — you own its lifecycle. Safe to call more than once.

Reconnection​

The socket gateway has Connection State Recovery enabled. If the socket drops and reconnects within the recovery window (~2 minutes), the gateway transparently restores your subscription and replays every event you missed during the gap — progress, partial results, and custom events. You don't do anything; run() and subscribe() just keep delivering.

If the disconnect is longer than the window, the session can't be recovered — the events are gone and the client fails fast so nothing hangs:

  • run() rejects with a WorkflowsError (503, reason: 'socket_connection_lost').
  • subscribe()'s for await loop throws the same error.

In both cases, re-run or re-subscribe to start a fresh stream.

Channels​

You don't set channels directly — run and subscribe derive them from name/taskId. For reference, events are delivered on these gateway channels:

  • workflows:all — every task of every workflow (firehose).
  • workflows:<workflow>:all — every task of one workflow (subscribe({ name }), and what run uses).
  • workflows:<workflow>:<taskId> — a single task (subscribe({ name, taskId })).