Skip to content

Getting started

Rungs is a library: your application calls it to send a record (an expense, a leave request, a purchase order) through approval steps, and Rungs decides who must approve, keeps the state and writes the audit trail. It runs in your Node.js process, on your database.

Terminal window
npm install @rungsjs/core @rungsjs/store-postgres

Rungs needs Node.js 22 or newer and is published as ES modules with TypeScript types. @rungsjs/store-postgres keeps everything in PostgreSQL 15 or newer; for tests and prototypes, @rungsjs/store-memory keeps it in memory.

An expense goes to the requester’s manager, then to finance when it is above 1,000:

import { createRungs } from "@rungsjs/core";
import { memoryStore } from "@rungsjs/store-memory";
const managers: Record<string, string> = { u_alice: "u_bob" };
const rungs = createRungs({
store: memoryStore(),
clock: () => new Date(),
ids: () => crypto.randomUUID(),
roles: async (role) => (role === "finance" ? ["u_fin1", "u_fin2"] : []),
resolvers: {
directManager: async ({ request }) => {
const manager = managers[request.requesterId];
return manager ? [manager] : [];
},
},
});
await rungs.flows.register({
key: "expense",
steps: [
{ id: "manager", approvers: { type: "resolver", name: "directManager" } },
{
id: "finance",
approvers: { type: "role", role: "finance" }, // either finance user may decide
when: { field: "amount", op: "gt", value: 1000 },
deadline: "P2D",
},
],
});
const request = await rungs.submit({
flow: "expense",
requesterId: "u_alice",
subject: { type: "expense", id: "EXP-1042" },
payload: { amount: 2500 },
});
await rungs.approve({ requestId: request.id, actorId: "u_bob", comment: "Looks fine" });
const done = await rungs.approve({ requestId: request.id, actorId: "u_fin2" });
console.log(done.status); // "approved"

Runs in CI:examples/docs-snippets/src/quick-start.ts

What happened:

  • createRungs gets a store and the things Rungs never decides by itself: the time, how to make IDs, and how to find approvers. roles turns a role into users; directManager is a resolver you write, here from a lookup table.
  • flows.register stores a versioned flow. Each request keeps the version it started with.
  • submit starts a request. Both steps apply because 2,500 is above 1,000, so Bob gets a task first.
  • approve records each decision. Either finance user may decide, so Bob’s and one finance approval complete the request.