@stateway/frontend-sdk
v0.4.0
Published
Stateway Frontend Gateway SDK — WebSocket client for browser frontends
Maintainers
Readme
@stateway/frontend-sdk
JavaScript/TypeScript client for Stateway — connect your frontend to BPMN process workflows in real-time.
Install
npm install @stateway/frontend-sdkZero runtime dependencies. Works natively in all modern browsers; pass webSocketImpl for Node.js.
Quick start
import { StatewayClient } from '@stateway/frontend-sdk';
const sw = new StatewayClient({
endpoint: 'wss://ws.stateway.io',
sessionToken: 'sws_live_...',
});
sw.onTaskFlow({
onTask: (task) => renderForm(task),
onComplete: () => showSuccess(),
});That's it. onTaskFlow handles the entire lifecycle: receiving tasks, tracking process progress, and detecting completion.
The work.* event lifecycle
When a session connects, the server immediately sends a work state event. There are three states:
connect
│
▼
work.loading — server is computing the user's work state
│
├─► work.snapshot — user has pending tasks (WorkItem[]) + their open instances
│
└─► work.empty — user has no pending tasks + their open instanceswork.loading — emitted right after connect. Show a loading indicator.
work.snapshot — the user has at least one pending task. The event delivers:
tasks: WorkItem[]— the list of tasks waiting for the userinstances: InstanceSummary[]— all process instances associated with the user (including those without a pending task)
work.empty — the user has no pending tasks. The event delivers:
definitions: Array<{ key, canStart }>— which processes the user can startinstances: InstanceSummary[]— all process instances still running for the user
instances is the key to answering "does this user have open process instances right now?" — even when they have no active task. For example, a support ticket that is awaiting an agent reply still appears in instances.
Use instances.length === 0 (not the absence of tasks) to determine whether a process is truly complete from the user's perspective.
Events
For lower-level control, subscribe to events directly:
sw.on('connected', () => console.log('ready'));
sw.on('work.loading', () => {
showSpinner();
});
sw.on('work.snapshot', (tasks, instances) => {
// tasks: WorkItem[] — pending tasks for this user
// instances: InstanceSummary[] — all open process instances
renderTaskList(tasks);
renderInstanceList(instances);
});
sw.on('work.empty', ({ definitions, instances }) => {
// instances: InstanceSummary[] — running instances with no pending task for the user
if (instances.length > 0) {
showWaitingState(instances); // e.g. "Your ticket is with the support team"
} else {
showNoWorkState(definitions); // e.g. "No open requests. Start a new one?"
}
});
sw.on('task.assigned', (task) => {
// a new task was assigned to the current user
console.log(task.name, task.variables);
});
sw.on('task.completed', ({ next }) => {
// task was completed; process is now at `next.elementName`
});
sw.on('notification', ({ message, variables }) => {
// process sent a notification message
});
sw.on('error', (err) => {
console.error(err.code, err.message);
});Actions
// start a new process instance
const { instanceId } = await sw.startProcess('loan-approval', {
amount: 50000,
applicant: '[email protected]',
});
// claim a task (marks it as in-progress by the current user)
await sw.claimTask(task.taskId);
// complete a task with output variables
await sw.completeTask(task.taskId, { approved: true, comment: 'looks good' });
// release a claimed task back to the pool
await sw.unclaimTask(task.taskId);
// send an event to a running instance (e.g. trigger a boundary event)
await sw.sendEvent(instanceId, 'payment_received', { amount: 50000 });
// evaluate a DMN decision table
const result = await sw.evaluateDecision('credit-score', { income: 80000 });
// rotate the session token without reconnecting
await sw.rotateToken('sws_live_newtoken...');
// close the connection
sw.disconnect();onTaskFlow helper
onTaskFlow is a high-level helper that maps the full task lifecycle to simple callbacks. It returns a cleanup function.
const cleanup = sw.onTaskFlow({
onTask(task) {
// called when a task is available (snapshot or newly assigned)
setCurrentTask(task);
},
onProcessing(elementName) {
// called after task completion while process executes automatic steps
setStatus(`Processing: ${elementName}...`);
},
onNotification(message, variables) {
// called when the process sends a notification
showToast(message);
},
onComplete() {
// called when there are no more open process instances (instances.length === 0)
setStatus('Done');
},
});
// later, to stop listening:
cleanup();Error handling
Actions reject with a StatewayError on failure. Check err.code for the error type:
try {
await sw.claimTask(taskId);
} catch (err) {
if (err.code === 'CLAIM_CONFLICT') {
alert('Someone else claimed this task');
}
}| Code | Meaning |
|---|---|
| CLAIM_CONFLICT | Task was already claimed by another user |
| CLAIM_REQUIRED | Task must be claimed before completion |
| FORBIDDEN | Session does not have permission for this action |
| TASK_NOT_FOUND | Task ID does not exist or belongs to another tenant |
| SCOPE_VIOLATION | Action not allowed at the current process state |
| SESSION_EXPIRED | Session token has expired — request a new one |
| CONNECTION_LOST | Request was pending when the connection dropped |
| TIMEOUT | Server did not respond within 30 seconds |
Session lifecycle
sw.on('session.expiring', ({ expiresIn }) => {
// expiresIn: seconds until expiry — fetch a new token
fetchNewToken().then((t) => sw.rotateToken(t));
});
sw.on('session.superseded', () => {
// another tab/device opened the same session — this connection is closed
showReconnectPrompt();
});The client reconnects automatically after network interruptions (exponential backoff, up to 30 s). It does not reconnect after session.superseded or an explicit disconnect().
TypeScript
All events and action return types are fully inferred — no casts needed:
sw.on('work.snapshot', (tasks, instances) => {
// tasks: WorkItem[]
// instances: InstanceSummary[]
tasks[0].taskId; // string
tasks[0].variables; // Record<string, unknown>
instances[0].instanceId; // string
instances[0].status; // 'running' | 'suspended'
});Exported types: WorkItem, InstanceSummary, WorkEmptyState, TaskCompletedEvent, NotificationEvent, StatewayError, StatewayClientOptions, EventMap, ErrorCode.
Node.js
WebSocket is not available globally in Node.js 20. Inject the ws package:
import { WebSocket } from 'ws';
import { StatewayClient } from '@stateway/frontend-sdk';
const sw = new StatewayClient({
endpoint: 'wss://ws.stateway.io',
sessionToken: 'sws_live_...',
webSocketImpl: WebSocket as unknown as typeof globalThis.WebSocket,
});Issues
Found a bug or have a question? Open an issue at github.com/stateway-io/stateway-issues.
License
MIT
