Running workflows with Node.js
Use Node.js 22 or newer and a disposable workspace. Set STEADYDO_API_KEY without printing it; the quickstart shows a masked prompt. Copy this helper and the JavaScript block from a workflow into one .mjs file. Blocks name their prerequisites. For the complete sequence, run bootstrap workspace, manage standalone work, work Next Up, recover conflicts, create custom process, maintain template, create/operate order, divide/duplicate, archive/recover, then rotate key. Finish with the cleanup block below to permanently remove the fictional resources through inspected previews.
The complete sequence needs all six scopes because it includes configuration, archive, previewed deletion and disposable-key management. Narrow a key to the listed scopes for a single workflow. Keep returned IDs in this local script context; do not guess them. Unexpected statuses stop execution and mutations are never retried automatically.
const base = (process.env.STEADYDO_BASE || 'https://dashboard.steadydo.com/api/v1').replace(/\/$/, '');
const key = process.env.STEADYDO_API_KEY;
if (!key) throw new Error('Set STEADYDO_API_KEY');
const run = `docs-${crypto.randomUUID()}`;
let sequence = 0;
function check(condition, message) { if (!condition) throw new Error(message); }
async function request(method, route, body, etag, expected = 200, credential = key, identity) {
const headers = { Authorization: `Bearer ${credential}`, Accept: 'application/json' };
if (body !== undefined) {
headers['Content-Type'] = 'application/json';
headers['Idempotency-Key'] = identity || `${run}-${++sequence}`;
}
if (etag) headers['If-Match'] = etag;
const response = await fetch(`${base}${route}`, { method, headers, redirect: 'error', ...(body === undefined ? {} : { body: JSON.stringify(body) }) });
const value = await response.json();
check(response.status === expected, `${method} ${route}: HTTP ${response.status}; ${value.code || 'unexpected response'}; request ${value.requestId || value.meta?.requestId || 'unknown'}`);
return { data: value.data, problem: value, etag: response.headers.get('etag'), headers: response.headers };
}
const get = route => request('GET', route);
async function action(route, body = {}, method = 'POST', readRoute = route.replace(/\/[^/]+$/, ''), expected = 200) {
const current = await get(readRoute);
check(current.etag, `No ETag for ${readRoute}`);
return request(method, route, body, current.etag, expected);
}
async function allPages(route) {
const rows = [];
let cursor;
do {
const page = await get(`${route}${route.includes('?') ? '&' : '?'}limit=25${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`);
rows.push(...page.data.items);
cursor = page.data.nextCursor;
} while (cursor);
return rows;
}Each guide asserts expected state. On failure inspect the request ID and persisted resources; do not rerun the entire sequence with new keys indiscriminately. An uncertain write must retain its original identity as described in idempotency. Local HTTP execution is not deployed-environment certification.
Clean up the combined fictional workflow
Run this final block only after all Node workflow examples completed in the same script. It permanently deletes the three exact example orders, the custom process and its five example items, and the example company. The task example already deleted its task and the rotation example revoked its keys. Inspect every preview before adapting this to real data. Base system configuration created by bootstrap is retained because it is required workspace configuration; it is not a disposable custom process. Audit history and revoked-key records follow the documented retention policy.
async function deleteExample(route) {
let current = await get(route);
if (!current.data.archived) await request('POST', `${route}/archive`, {}, current.etag);
current = await get(route);
const preview = await request('POST', `${route}/delete-preview`, {}, current.etag);
check(preview.data.effects && preview.data.confirmationToken, 'Cleanup preview missing');
await request('POST', `${route}/permanent-delete`, {confirmationToken:preview.data.confirmationToken}, current.etag);
await request('GET', route, undefined, undefined, 404);
}
for (const id of [order.id, duplicate.id, divided.data.created.id]) await deleteExample(`/orders/${id}`);
const cleanupProcessRoute = `/processes/${processId}`;
let cleanupProcess = await get(cleanupProcessRoute);
const archiveProcessPreview = await request('POST', `${cleanupProcessRoute}/archive-preview`, {}, cleanupProcess.etag);
check(archiveProcessPreview.data.effects, 'Process archive effects missing');
await request('POST', `${cleanupProcessRoute}/archive`, {confirmationToken:archiveProcessPreview.data.confirmationToken}, cleanupProcess.etag);
cleanupProcess = await get(cleanupProcessRoute);
const configurationPreview = await request('POST', `${cleanupProcessRoute}/delete-preview`, {}, cleanupProcess.etag);
await request('POST', `${cleanupProcessRoute}/permanent-delete`, {confirmationToken:configurationPreview.data.confirmationToken}, cleanupProcess.etag);
// Configuration deletion deliberately retains an immutable snapshot and item history.
// Rediscover that snapshot's version through the public export before final deletion.
const retainedSnapshot = (await allPages('/workspace/export')).find(row => row.type === 'deletedProcessSnapshots' && row.value.sourceProcessId === processId);
check(retainedSnapshot, 'Retained process snapshot missing');
const snapshotETag = `"v${retainedSnapshot.value.version}"`;
const finalProcessPreview = await request('POST', `${cleanupProcessRoute}/delete-preview`, {}, snapshotETag);
check(finalProcessPreview.data.effects, 'Final process effects missing');
await request('POST', `${cleanupProcessRoute}/permanent-delete`, {confirmationToken:finalProcessPreview.data.confirmationToken}, snapshotETag);
await request('GET', `${cleanupProcessRoute}/items`, undefined, undefined, 404);
await deleteExample(`/companies/${company.id}`);