Official TypeScript/JavaScript SDK for the QuantumBPM platform - DMN evaluation, BPMN process orchestration, and external job workers.
npm install @quantumbpm/sdkNode.js 18+ (uses native AbortController and AbortSignal).
| Module | Purpose |
|---|---|
QuantumBPM |
Top-level client exposing .dmn and .bpmn, plus newWorker(...) |
ZitadelTokenProvider, StaticTokenProvider |
Authentication. Implement TokenProvider for BYO providers |
DmnClient |
DMN evaluation: stored definitions, ad-hoc XML, batch |
BpmnClient |
BPMN resources, instances, messaging, user tasks, processes |
Worker |
External job worker runtime - long-poll, lock heartbeat, dispatch |
Vars |
Variables wrapper with typed accessors and FEEL-context conversion |
RawClient |
OpenAPI-generated client. Reachable via client.raw, never hand-edited |
import { QuantumBPM, Vars, ZitadelTokenProvider } from '@quantumbpm/sdk';
const provider = new ZitadelTokenProvider(
'./service-account.json', // Zitadel JSON Key file
'https://auth.quantumbpm.com', // issuer
'your-zitadel-project-id', // audience scope
);
const client = new QuantumBPM({
baseUrl: 'https://api.quantumbpm.com',
projectId: '00000000-0000-0000-0000-000000000000',
tokenProvider: provider,
});
const result = await client.dmn.evaluate(
'loan-eligibility',
new Vars().set('requestedAmt', 1000).set('creditScore', 720),
);
console.log(result);The TokenProvider interface returns a bearer token on each request. Two implementations ship out of the box.
const provider = new ZitadelTokenProvider(
'./service-account.json', // path to JSON Key file
'https://auth.quantumbpm.com', // issuer URL
'your-zitadel-project-id', // adds the audience scope
);The provider caches tokens in-memory until shortly before expiry.
For Enterprise deployments that issue long-lived API keys, or in tests where a token is acquired out of band:
import { StaticTokenProvider } from '@quantumbpm/sdk';
const provider = new StaticTokenProvider('eyJhbGciOi...');Implement the interface:
import { TokenProvider } from '@quantumbpm/sdk';
class MyProvider implements TokenProvider {
async getToken(): Promise<string> {
// ... your auth logic
return token;
}
}The client.dmn sub-client offers four methods.
const result = await client.dmn.evaluate(
'loan-eligibility',
new Vars().set('requestedAmt', 5000).set('creditScore', 720),
);Returns Record<string, EvaluationResult> keyed by decision name. Each result has value, hitRules, error, and type.
Pin a version, restrict the evaluated decisions, or attach decision services:
const result = await client.dmn.evaluate('loan-eligibility', vars, {
version: 3,
decisions: ['eligibility', 'rate'],
});When you already hold a database-version pointer:
const result = await client.dmn.evaluateById(definitionUuid, vars);For "evaluate while editing" flows that don't store the XML:
const result = await client.dmn.evaluateDesign(dmnXml, vars, {
additionalXMLs: [importedXml1, importedXml2],
decisions: ['eligibility'],
});const rows = [
new Vars().set('requestedAmt', 1000),
new Vars().set('requestedAmt', 5000),
new Vars().set('requestedAmt', 25000),
];
const batch = await client.dmn.evaluateDesignBatch(dmnXml, rows);client.bpmn covers the full BPMN runtime surface.
// Stage a BPMN draft, then deploy it.
const draft = await client.bpmn.createResource('loan-process', bpmnXml);
await client.bpmn.deployResource(draft.id);
// Re-fetch to get the populated process-definition list.
const deployed = await client.bpmn.getResource(draft.id);
const processDef = deployed.processes![0];
// Start an instance.
const workflowId = await client.bpmn.startInstance(
processDef.id,
new Vars().set('applicantID', 'u-123').set('requestedAmt', 25000),
);const state = await client.bpmn.getInstance(workflowId);
console.log(state.status, state.activeScopes);
const vars = await client.bpmn.getInstanceVariables(workflowId);
const children = await client.bpmn.getInstanceChildren(workflowId);await client.bpmn.publishMessage(
'loan-approved',
new Vars().set('approvedAmt', 24000),
{
correlationKeys: { /* ... */ },
ttl: 'PT5M',
},
);
await client.bpmn.publishSignal('system-maintenance', new Vars());const page = await client.bpmn.listUserTasks({
assignee: 'alice@example.com',
status: 'CREATED',
});
await client.bpmn.completeUserTask(executionKey, new Vars().set('approved', true));
// Or fail with a BPMN error code (matches boundary error events):
await client.bpmn.throwUserTaskError(executionKey, 'REVIEW_REJECTED', new Vars());Workers handle service tasks asynchronously. Register a handler per task type, then call run. The runtime owns long-polling, lock heartbeats, dispatch, and outcome mapping.
import { QuantumBPM, Vars, BpmnError } from '@quantumbpm/sdk';
const ac = new AbortController();
const worker = client.newWorker({ clientId: 'billing-svc' });
worker.handle('send-email', async (job) => {
const recipient = job.vars.get<string>('recipient');
const subject = job.vars.get<string>('subject');
await emailer.send(recipient, subject);
return new Vars().set('messageID', 'msg-123'); // → Complete
});
await worker.run(ac.signal);run() resolves when the AbortSignal is triggered, after in-flight handlers settle.
worker.handle('send-email', handler, {
maxJobs: 10, // up to 10 in flight per task type
pollTimeout: '45s', // long-poll wait
lockDuration: '2m', // exclusive lock per job
});Concurrency is per task type. Different task types run independently. The runtime auto-renews the lock at half the lock-duration interval while the handler runs.
Throw a BpmnError to fail the job with a code that boundary error events on the originating service task can catch:
worker.handle('charge-card', async (job) => {
try {
const txId = await charge(job.vars.toRecord());
return new Vars().set('transactionID', txId);
} catch (err) {
if (err.code === 'INSUFFICIENT_FUNDS') {
throw new BpmnError('INSUFFICIENT_FUNDS', new Vars().set('availableBalance', 12.0));
}
throw err; // → ThrowError("WORKER_ERROR"); retry budget decrements
}
});Handlers that throw anything other than BpmnError report WORKER_ERROR and the server treats it as a retryable failure.
Generic on handle() decodes the job's input variables into a typed shape. The SDK does not validate at runtime - TypeScript treats it as a hint:
interface EmailJob {
recipient: string;
subject: string;
}
worker.handle<EmailJob>('send-email', async (job) => {
// job.typed is EmailJob
await emailer.send(job.typed.recipient, job.typed.subject);
return new Vars().set('messageID', 'msg-123');
});Vars is a thin wrapper around Record<string, unknown> shared by DMN, BPMN, and workers.
const v = new Vars().set('amount', 100).set('name', 'Alice');
const v = Vars.from({ amount: 100, name: 'Alice' });const amount = v.get<number>('amount');
const flag = v.get<boolean>('approved');
interface Loan {
requestedAmt: number;
approved: boolean;
}
const loan = v.as<Loan>();get<T> and as<T> are TypeScript hints - no runtime validation. Combine with a runtime validator (e.g. Zod) at boundaries when you need it.
FEEL numbers are exact decimals server-side, but this SDK deserializes JSON with JSON.parse, so numbers arrive as IEEE-754 doubles (~15–17 significant digits). Values beyond double precision - e.g. 1234567890.123456789012345678 - are silently rounded on receipt, and echoing them back sends the rounded value. This is a JavaScript platform limitation; the Go, Java, and Python SDKs preserve exact decimals.
If you need currency-grade round-trips through a JS worker or client, represent those values as FEEL strings in your processes and convert with a decimal library (e.g. decimal.js, big.js) at the edges.
The client.raw property exposes the underlying generated client for endpoints not yet wrapped (instance migration, modification, ad-hoc triggers, batch job complete/error, etc.):
await client.raw.bpmn.migrateBpmnInstance(client.projectId, workflowId, body);MIT License - see LICENSE for details.