@stateway/frontend-sdk — Reference
Installation
npm install @stateway/frontend-sdk
# or
yarn add @stateway/frontend-sdk
Supports browsers and SSR environments (Next.js, Nuxt, etc.). No runtime dependencies beyond the package itself.
StatewayClient
import { StatewayClient } from '@stateway/frontend-sdk';
const sw = new StatewayClient({
endpoint: 'wss://ws.stateway.io', // required
sessionToken: 'sws_live_...', // required
reconnect: true, // default: true
reconnectMaxDelay: 30000, // default: 30000ms
debug: false, // default: false
});
Constructor Options
| Option | Type | Default | Description |
|---|---|---|---|
endpoint | string | — | WebSocket server URL |
sessionToken | string | — | Session token from POST /v1/sessions |
reconnect | boolean | true | Enable automatic reconnection |
reconnectMaxDelay | number | 30000 | Maximum reconnection backoff in milliseconds |
debug | boolean | false | Enable debug logging to console |
Events
Connection Events
sw.on('connected', () => void)
sw.on('disconnected', ({ code: number, reason: string }) => void)
Work Events
// Fired immediately on connect — connection is live, snapshot is loading
sw.on('work.loading', () => void)
// Fired once snapshot is ready (after work.loading) — user has pending tasks
sw.on('work.snapshot', (tasks: WorkItem[], instances: InstanceSummary[]) => void)
// Fired when there are no pending tasks
sw.on('work.empty', (state: WorkEmptyState) => void)
The work.* events follow a fixed lifecycle on every (re)connect:
work.loading → work.snapshot (user has pending tasks)
→ work.empty (no pending tasks)
work.loading — emitted synchronously on connection before the server runs the work-resolution query. Show a loading indicator.
work.snapshot — the user has at least one pending task.
tasks: WorkItem[]— the pending tasksinstances: InstanceSummary[]— all process instances associated with the user, including those without a pending task
work.empty — no pending tasks, but process instances may still be running.
state.definitions— which processes the user can startstate.instances: InstanceSummary[]— running process instances with no pending task for this user
instances.length is the canonical check for whether a user has open process activity. For example, a support ticket waiting for an agent is visible in instances even though the user has no task to complete.
Task Events
// A new task was assigned to the user
sw.on('task.assigned', (task: TaskItem) => void)
// A task the user completed has moved on; process advanced
sw.on('task.completed', (result: TaskCompletedEvent) => void)
// Claim confirmed for the user who made the claim (update isClaimed locally)
sw.on('task.claimed', (event: TaskClaimedEvent) => void)
// A pool task was claimed by another user (remove or disable task in UI)
sw.on('task.claimed_by_other', ({ taskId: string, instanceId: string, claimedBy: string }) => void)
task.claimed is sent only to the user who performed the claim. Use it to update isClaimed and show the task form without optimistic updates. task.claimed_by_other is sent to all other eligible users.
Instance Events
// A process instance reached a terminal state
sw.on('instance.completed', (event: InstanceCompletedEvent) => void)
Fired when a process instance transitions to completed, terminated, or error.
| Field | Type | Description |
|---|---|---|
instanceId | string | ID of the instance that ended |
status | 'completed' | 'terminated' | 'error' | Final state of the instance |
definitionKey | string | Process definition key |
variables | object | Final instance variables (only populated with variable_visibility: 'all'; {} with 'declared') |
endedAt | string (ISO 8601) | Timestamp of when the instance ended |
Use instance.completed to track process history in the UI, show completion toasts, or navigate away from an active process view. This event is the reliable alternative to polling.
sw.on('instance.completed', ({ instanceId, status, variables }) => {
if (status === 'completed') {
showToast(`Process finished — rating: ${variables.rating}`);
} else if (status === 'terminated') {
showToast(`Process was cancelled.`);
}
removeFromActiveList(instanceId);
});
Session Events
// Session is approaching expiry
sw.on('session.expiring', ({ expiresIn: number }) => void)
// This session was superseded by a new session for the same user_id
sw.on('session.superseded', () => void)
Error Event
sw.on('error', (error: StatewayError) => void)
Actions
All action methods return Promises and are correlated by a unique request ID internally.
startProcess
const instance = await sw.startProcess(
definitionKey: string,
variables?: Record<string, unknown>
): Promise<{ instanceId: string; status: string }>
Starts a new process instance. Requires can_start: true in the session scope for that definition.
claimTask
await sw.claimTask(taskId: string): Promise<void>
Claims a pool task (candidateUsers or candidateGroups). The user must be eligible. Returns CLAIM_CONFLICT if another user claimed first.
unclaimTask
await sw.unclaimTask(taskId: string): Promise<void>
Releases a claim previously made by this user.
completeTask
await sw.completeTask(
taskId: string,
variables?: Record<string, unknown>
): Promise<void>
Completes a task with output variables. For pool tasks, the user must have claimed the task first; otherwise returns CLAIM_REQUIRED.
sendEvent
await sw.sendEvent(
instanceId: string,
eventName: string,
variables?: Record<string, unknown>
): Promise<void>
Sends an event to a process instance (e.g., to trigger a boundary event or intermediate catch event).
evaluateDecision
const result = await sw.evaluateDecision(
decisionKey: string,
variables: Record<string, unknown>
): Promise<Record<string, unknown>>
Evaluates a DMN decision model and returns the result.
rotateToken
await sw.rotateToken(newToken: string): Promise<void>
Replaces the session token without disconnecting. Use when your backend has issued a refreshed token.
search
const result = await sw.search({
definitionKey: string,
definitionVersion?: string | number,
statusScope?: string | string[],
filter?: Record<string, unknown>,
sort?: Record<string, number>,
page?: { limit?: number; cursor?: string | null },
projection?: Record<string, 1>,
text?: { query: string; mode?: 'fulltext' | 'trigram'; fields?: string[]; fuzzy?: boolean },
}): Promise<SearchResult>
Searches process instances by variable values. definitionKey is required and must be in the session scope. Only variables listed in searchable_variables (set during session creation) are visible in the filter and results.
const result = await sw.search({
definitionKey: 'loan-application',
filter: {
'variables.loan_status': 'in_review',
'variables.amount': { $gte: 50000 },
},
page: { limit: 20 },
});
// next page
if (result.nextCursor) {
const page2 = await sw.search({
definitionKey: 'loan-application',
filter: { 'variables.loan_status': 'in_review' },
page: { limit: 20, cursor: result.nextCursor },
});
}
Throws SCOPE_VIOLATION if any field in filter or projection is not in searchable_variables. See Query Language for filter syntax.
disconnect
sw.disconnect(): void
Closes the WebSocket connection and stops reconnection.
onTaskFlow Helper
A high-level helper for single-process wizard flows:
const cleanup = sw.onTaskFlow({
onTask?: (task: WorkItem) => void,
onProcessing?: (elementName: string) => void,
onNotification?: (message: string, variables: Record<string, unknown>) => void,
onComplete?: () => void,
onInstanceCompleted?: (event: InstanceCompletedEvent) => void,
}): () => void // returns cleanup function
Returns a cleanup function. Call it when the component unmounts to remove all listeners.
Types
WorkItem
interface WorkItem {
kind: 'task';
taskId: string;
instanceId: string;
definitionKey: string;
name: string;
assignee: string | null; // null for pool tasks
candidateUsers: string[];
candidateGroups: string[];
isClaimed: boolean;
claimedBy: string | null; // user_id of who claimed; null if unclaimed
formSchema: JSONSchema | null;
variables: Record<string, unknown>;
createdAt: string;
dueAt: string | null;
}
InstanceSummary
interface InstanceSummary {
instanceId: string;
definitionKey: string;
status: 'running' | 'suspended';
startedAt: string; // ISO 8601
}
Included in both work.snapshot (second argument) and work.empty (state.instances). Represents a process instance associated with the user — whether or not the user currently has a task in that instance.
WorkEmptyState
interface WorkEmptyState {
definitions: Array<{
key: string;
canStart: boolean;
}>;
instances: InstanceSummary[];
}
TaskCompletedEvent
interface TaskCompletedEvent {
taskId: string;
instanceId: string;
next: {
kind: 'automatic' | 'waiting_event' | 'timer';
elementName: string;
estimatedFireAt: string | null; // set when kind === 'timer'
};
}
TaskClaimedEvent
interface TaskClaimedEvent {
taskId: string;
instanceId: string;
claimedBy: string; // user_id of the user who made the claim (always the current user)
}
NotificationEvent
interface NotificationEvent {
instanceId: string;
elementId: string;
message: string;
variables: Record<string, unknown>;
}
InstanceCompletedEvent
interface InstanceCompletedEvent {
instanceId: string;
status: 'completed' | 'terminated' | 'error';
definitionKey: string;
variables: Record<string, unknown>;
endedAt: string;
}
StatewayError
interface StatewayError {
code: string;
message: string;
requestId?: string;
}
SearchQuery
interface SearchQuery {
definitionKey: string; // required; must be in session scope
definitionVersion?: string | number;
statusScope?: string | string[]; // 'active' (default), 'completed', 'all'
filter?: Record<string, unknown>;
sort?: Record<string, number>;
page?: { limit?: number; cursor?: string | null };
projection?: Record<string, 1>;
text?: SearchTextQuery;
}
SearchTextQuery
interface SearchTextQuery {
query: string;
mode?: 'fulltext' | 'trigram';
fields?: string[];
fuzzy?: boolean;
}
InstanceResult
interface InstanceResult {
instanceId: string;
definitionKey: string;
definitionVersion: number;
correlationId: string | null;
status: string;
startedAt: string; // ISO 8601
endedAt: string | null;
variables: Record<string, unknown>; // filtered by searchable_variables
metadata?: Record<string, unknown>;
}
SearchStats
interface SearchStats {
returned: number;
limit: number;
scanMode: string; // e.g. 'gin_index', 'seq_scan'
textMode: string | null;
estimatedComplexity: string;
}
SearchResult
interface SearchResult {
results: InstanceResult[];
nextCursor: string | null;
stats: SearchStats;
}
Error Codes
| Code | Situation |
|---|---|
CLAIM_CONFLICT | Another user won the claim simultaneously |
CONFLICT | Task completion rejected. Two distinct causes: (1) the underlying execution token was already dead (e.g. the process instance already errored for an unrelated reason) — recovery (e.g. token move) is needed before the task can be completed; (2) another concurrent request already completed or cancelled this same task and won the race — the task is already in its final state and the instance is healthy, so re-fetch the task instead of retrying. Differs from TASK_NOT_FOUND below: TASK_NOT_FOUND fires for a sequential second attempt against a task that is already completed/cancelled by the time this request starts reading it; CONFLICT fires when this request raced a concurrent completion attempt in flight and lost. |
VALIDATION_ERROR | Task completion rejected. Two causes: (1) the variables patch is not a plain object; (2) the merged payload would exceed the variables size cap — for both, nothing is left inconsistent, reverting the task back to its pre-request state if a concurrent write only trips the size check after human_tasks was already mutated. Unlike the Human Tasks API, activityContract output validation is not enforced on this path — the internal route the SDK's completeTask() calls does not implement it, only the public REST endpoint does. |
CLAIM_REQUIRED | Pool task — must claim before completing |
FORBIDDEN | User is not eligible for that task |
TASK_NOT_FOUND | Task was completed or cancelled by another path |
SCOPE_VIOLATION | Search field is not in searchable_variables, or definition is not in session scope |
SESSION_EXPIRED | Token expired — frontend must renew the session |