Skip to main content

@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

OptionTypeDefaultDescription
endpointstringWebSocket server URL
sessionTokenstringSession token from POST /v1/sessions
reconnectbooleantrueEnable automatic reconnection
reconnectMaxDelaynumber30000Maximum reconnection backoff in milliseconds
debugbooleanfalseEnable 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 tasks
  • instances: 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 start
  • state.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.

FieldTypeDescription
instanceIdstringID of the instance that ended
status'completed' | 'terminated' | 'error'Final state of the instance
definitionKeystringProcess definition key
variablesobjectFinal instance variables (only populated with variable_visibility: 'all'; {} with 'declared')
endedAtstring (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.

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

CodeSituation
CLAIM_CONFLICTAnother user won the claim simultaneously
CONFLICTTask 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_ERRORTask 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_REQUIREDPool task — must claim before completing
FORBIDDENUser is not eligible for that task
TASK_NOT_FOUNDTask was completed or cancelled by another path
SCOPE_VIOLATIONSearch field is not in searchable_variables, or definition is not in session scope
SESSION_EXPIREDToken expired — frontend must renew the session