Skip to content

About

Official TypeScript/JavaScript SDK for QuantumBPM: BPMN 2.0 workflow orchestration, DMN 1.5 decision evaluation with FEEL, and job workers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

QuantumBPM TypeScript/JavaScript SDK

Official TypeScript/JavaScript SDK for the QuantumBPM platform - DMN evaluation, BPMN process orchestration, and external job workers.

Installation

npm install @quantumbpm/sdk

Node.js 18+ (uses native AbortController and AbortSignal).

What's in the box

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

Quick start

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);

Authentication

The TokenProvider interface returns a bearer token on each request. Two implementations ship out of the box.

Zitadel service account

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.

Static bearer token

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...');

Bring your own

Implement the interface:

import { TokenProvider } from '@quantumbpm/sdk';

class MyProvider implements TokenProvider {
    async getToken(): Promise<string> {
        // ... your auth logic
        return token;
    }
}

DMN evaluation

The client.dmn sub-client offers four methods.

Evaluate a stored definition

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'],
});

Evaluate by platform UUID

When you already hold a database-version pointer:

const result = await client.dmn.evaluateById(definitionUuid, vars);

Ad-hoc XML evaluation

For "evaluate while editing" flows that don't store the XML:

const result = await client.dmn.evaluateDesign(dmnXml, vars, {
    additionalXMLs: [importedXml1, importedXml2],
    decisions: ['eligibility'],
});

Batch ad-hoc evaluation

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);

BPMN processes

client.bpmn covers the full BPMN runtime surface.

Deploy and start

// 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),
);

Inspect runtime state

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);

Send messages and signals

await client.bpmn.publishMessage(
    'loan-approved',
    new Vars().set('approvedAmt', 24000),
    {
        correlationKeys: { /* ... */ },
        ttl: 'PT5M',
    },
);

await client.bpmn.publishSignal('system-maintenance', new Vars());

User tasks

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());

External job workers

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.

Minimal worker

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 after the AbortSignal aborts and in-flight handlers settle. See Graceful shutdown.

Concurrency, polling, and locks

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 server clamps lockDuration to between 10s and 5m by default (BPMN_WORKER_LEASE_MIN / BPMN_WORKER_LEASE_MAX) and returns the expiry it granted with each job (lockExpiresAt). While the handler runs, the runtime heartbeats at half of the granted lock, or half of lockDuration when that is shorter, with a 1s floor. A lockDuration of '11m' therefore heartbeats every 150s under the default 5m cap.

lockDuration, pollTimeout and shutdownTimeout take Go duration strings, the format the server parses: a sequence of numbers with optional fractions and a unit each, such as '30s', '12.5s', '1m30s', '500ms' or '1.5h'. Units are ns, us (or µs), ms, s, m and h. An invalid value throws: handle() throws for lockDuration and pollTimeout, newWorker() throws for shutdownTimeout.

Lost locks

Each polled job is locked to the worker's clientId, and heartbeats keep that lock while the handler runs. The server caps one attempt at 30 minutes by default (BPMN_JOB_MAX_HOLD). If the lock runs out, the server charges the job a retry and offers it again, or raises an incident when no retries are left.

When a heartbeat finds the lock gone, the runtime aborts the handler's signal and reports nothing for that job. A result reported after the lock is gone is discarded by the server and logged as a warning. Pass the signal to your I/O so the handler stops early:

worker.handle('fetch-report', async (job, signal) => {
    const res = await fetch(job.vars.get<string>('url'), { signal });
    return new Vars().set('status', res.status);
});

Graceful shutdown

Abandoning a running job costs it a retry, so stop the worker cleanly. When the signal passed to run() aborts:

  • Polling stops at once and the in-flight long-poll is cancelled.
  • Running handlers keep their locks and keep heartbeating. Their results are reported as usual.
  • run() resolves once they finish, or after shutdownTimeout (default '20s'). Handlers still running then have their signal aborted and report nothing. Their locks run out on the server.
const worker = client.newWorker({ clientId: 'billing-svc', shutdownTimeout: '20s' });
// ...register handlers

const ac = new AbortController();
process.once('SIGTERM', () => ac.abort());
await worker.run(ac.signal);

On Kubernetes, set terminationGracePeriodSeconds above the shutdown timeout so the pod is not killed while handlers finish. The Kubernetes default of 30s fits the 20s default.

Throwing typed BPMN errors

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.

Typed handlers

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');
});

Variables

Vars is a thin wrapper around Record<string, unknown> shared by DMN, BPMN, and workers.

Construction

const v = new Vars().set('amount', 100).set('name', 'Alice');
const v = Vars.from({ amount: 100, name: 'Alice' });

Typed access

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.

Number precision

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.

Escape hatch

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);

License

MIT License - see LICENSE for details.

About

Official TypeScript/JavaScript SDK for QuantumBPM: BPMN 2.0 workflow orchestration, DMN 1.5 decision evaluation with FEEL, and job workers.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages