Design sketches
Ten sketches of how qlane might be used. They show intent, not a finished API.
Every sketch on this page is a design sketch, not working code; names are proposals. The API names are drafts and will change. Nothing can be installed or run today.
1. Basic ticket with saved steps
Shows a two-step ticket that resumes after a crash without paying for the first step again.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 1. Basic ticket with saved steps
import { qlane } from "qlane"; // Proposed name
const q = qlane.embedded({ file: "./qlane.db" }); // Proposed constructor
q.workflow("report.weekly", async (ctx, input) => { // Proposed
const summary = await ctx.step("summary", input, async (i) => {
return await ctx.providers.anthropic.messages({ model: "<model-id>", maxTokens: 2000 /* ... */ });
});
const review = await ctx.step("review", { summary }, async (i) => {
return await ctx.providers.anthropic.messages({ model: "<model-id>", maxTokens: 8000 /* ... */ });
});
return review;
});
const ticket = await q.submit("report.weekly", { id: "w42", page: "team-weekly" }, {
key: "report-w42", // submitting this key again returns the same ticket
labels: { user: "u_123", plan: "pro", app: "myapp" },
});
// If the worker dies after "summary" is saved, the retry skips "summary"
// and continues at "review". "summary" is not called or paid for again.
const done = await ticket.wait(); // Proposed method name 2. Spend caps and provider fallback
Shows spend caps per ticket, user and plan, and an ordered fallback between providers.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 2. Spend caps and provider fallback
q.configure({ // Proposed
budgets: [
{ scope: "ticket", limits: { usd: 5 } },
{ scope: "user", limits: { usd: 20 }, window: "month" },
{ scope: "plan", match: { plan: "free" }, limits: { tokens: 1_000_000 }, window: "month" },
],
prices: { "anthropic:<model-id>": { inputPerMTok: 0, outputPerMTok: 0 } }, // you supply real prices
onExceed: "stop_keep_partial", // the default: stop, keep finished steps
});
await ctx.step("review", { summary }, {
maxTokens: 8000, // required when a budget applies
fallback: [ // tried in order; moving on does not use an attempt
{ provider: "anthropic", model: "<model-id>" },
{ provider: "openai", model: "<model-id>", when: ["provider_rate_limited", "server_error"] },
],
}, async (i, ctx) => { /* ... */ });
// A ticket that would break a cap is refused at submit time (budget_would_exceed).
// One that hits a cap while running ends as "budget_exceeded" with its partial results kept. 3. Effect ledger and unknown outcome
Shows how a step with a side effect is recorded, and what happens when a crash leaves its outcome unknown.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 3. Effect ledger and unknown outcome
await ctx.step("publish", { page: input.page }, {
effect: true, // intent is written before, outcome after
onUnknown: "pause", // default: stop and ask a person
probe: async (args) => ({ happened: await wiki.hasPost(args.page) }), // optional check
}, async (input) => {
return await wiki.publish(input.page /* ... */);
});
// Crash between "about to publish" and "published":
// with a probe -> qlane runs it and records the answer
// without a probe -> the ticket becomes "unknown_outcome" and waits for a person:
//
// $ qlane tickets resolve tkt_01H... --step publish --decision confirmed_not_done
//
// Options: confirmed_done, confirmed_not_done, or retry (runs the probe again;
// refused when there is no probe). 4. Async provider job (rendering a chart image)
Shows a long provider job that is submitted once and then polled by its saved job id.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 4. Async provider job (rendering a chart image)
const chart = await ctx.step.async("chart", input, { // Proposed
submit: async (i, c) => ({ jobId: await imageApi.create(i /* ... */) }),
poll: async (jobId, c) => imageApi.status(jobId), // { state: "running" | "done" | "failed", result }
download: async (jobId, result, c) => imageApi.fetch(result.url),
lookup: async (clientRef, c) => imageApi.findByRef(clientRef), // optional: find a job after a lost answer
maxWait: "30m",
maxPolls: 400,
});
// The job id is saved the moment submit returns. A dropped connection while polling,
// a crash or a restart resumes by that id and never submits again.
// Between polls the ticket is parked: no worker, no slot. 5. Schedule with missing-run alert and receipt
Shows a scheduled daily job, the alert for a run that never happened, and a receipt of what it did.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 5. Schedule with missing-run alert and receipt
q.schedule({ // Proposed
name: "daily-report", // registered at startup by name; a restart makes no duplicate
kind: "cron", cron: "0 6 * * *", timezone: "Europe/London",
overlap: "skip", // default: skip a run if the previous one is not finished
workflow: "report.daily",
expectRun: { by: "06:30" }, // optional; without it a default grace period applies
});
q.workflow("report.daily", async (ctx) => {
const report = await ctx.step("build", {}, async () => buildReport());
await ctx.step("send", { report }, { effect: true }, async () => {
const n = await sendToTeam(report);
ctx.receipt(`daily report sent to ${n} people`); // Proposed: a short note of what happened
});
});
q.on("schedule.run_missing", (e) => {
// e.case is "missing" (never ran, failed or died), "no_receipt" (ran, saved no receipt) or "disabled" (the schedule was disabled when the run fell due)
page(`schedule ${e.scheduleId} did not run for ${e.scheduledFor}`);
}); 6. Crash-test harness
Shows a seeded crash test that kills workers and breaks providers, then checks the rules.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 6. Crash-test harness
import { harness } from "qlane/test"; // Proposed subpath
const run = await harness.run({
seed: 1234, // the same seed replays the same run
workflows: [reportWeekly],
faults: {
crash: { anywhere: true }, // kill a worker at any point
provider: { rateLimit: 0.1, slow: 0.05, dropStream: 0.02, truncate: 0.02 },
duplicates: true,
},
});
// Checks the rules on every run: no lost ticket, no double effect (or marked unknown),
// no double claim, a saved step is never paid for again.
run.assertInvariants();
// $ qlane test --seeds 500 # a failing seed is saved as a regression test 7. Embedded vs server setup
Shows the two ways to run qlane: inside your process on one SQLite file, or as a server with remote workers.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 7. Embedded vs server, set up with "qlane init"
// $ qlane init
// ? Mode: embedded | server
// Creates qlane.config, a database file and a sample workflow.
// Writes no provider keys to disk. Sends nothing anywhere.
// Embedded: one process, one SQLite file, no keys
import { qlane } from "qlane";
const q = qlane.embedded({ file: "./qlane.db" });
// Server: run "qlane serve", then connect
import { client } from "qlane/client"; // Proposed subpath
const remote = client({ url: "https://qlane.internal.example", key: process.env.QLANE_KEY });
await remote.submit("report.weekly", { id: "w42", page: "team-weekly" }, { key: "report-w42" });
// A worker, on another machine, registers the same workflow names
import { worker } from "qlane/worker"; // Proposed subpath
worker({ url: "https://qlane.internal.example", key: process.env.QLANE_WORKER_KEY })
.workflow("report.weekly", reportWeekly); 8. BullMQ migration importer
Shows what a dry run of the BullMQ importer would report, including flows it cannot import.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 8. BullMQ migration importer (flows are listed as unsupported)
// $ qlane bullmq migrate --redis redis://localhost:6379 --dry-run
//
// Queues: 2 found (renders, emails)
// Repeat jobs: 1 found (nightly-report: "0 3 * * *") -> schedule
// Retry options: attempts=5, backoff=exponential -> mapped to qlane retry settings
// Queued jobs: 37 queued, 4 delayed -> would be imported as tickets
// Flows: 3 parent/child flows found
// UNSUPPORTED: not imported. See the manual guide to rebuild them
// with ctx.submit and dependencies.
//
// The importer reads settings and imports waiting jobs. It does not convert your
// processor code: you write the qlane workflow yourself. 9. Child tickets with a dependency fan-out
Shows a ticket that creates child tickets and waits for them, with the milestones that plan each part.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 9. A ticket that creates child tickets and waits for all of them
q.workflow("report.render", async (ctx, input) => { // Proposed
const sections = await ctx.step("summary", input, async (i) => splitIntoSections(i));
// Plain submit (planned for M1): returns the child's id at once, nothing waits.
const audit = await ctx.submit("report.audit", { id: input.id });
// Fan-out with a dependency (planned for M3): the parent is paused with the
// reason "waiting_for_children", holding no worker or slot.
const results = await ctx.submit(
sections.map((s) => ({ workflow: "report.chart", input: s })),
{ dependency: true, onChildFail: "fail" }, // or "continue"
);
// Fan-in: resumes once every dependency child is final, with their results.
return await ctx.step("review", { results }, async (r) => assemble(r));
});
// Default child key: origin ticket id + step name + call index, so a retried
// parent never creates a second child (it finds the existing ones). Children inherit the parent's user, plan, app
// and queue caps. Cancelling the parent cancels unfinished dependency children;
// "audit" above is a plain child and is never cascaded.
// A loop guard refuses chains that are too deep or repeat the same work. 10. HR or IT assistant (internal)
Shows a spend cap and an effect step together, so that a retry cannot raise the same request twice.
Design sketch, not working code; names are proposals. Nothing here can be installed or run today.
// 10. An internal HR or IT assistant: a retry cannot raise the same request twice
q.configure({ // Proposed
budgets: [
{ scope: "ticket", limits: { usd: 5 } }, // the cap on one conversation
{ scope: "user", limits: { usd: 20 }, window: "month" }, // and on one person
],
});
q.workflow("assistant.request", async (ctx, input) => { // Proposed
// A saved step: the model reads the message and proposes one action,
// for example applying for leave or raising an IT ticket.
const plan = await ctx.step("decide", input, {
maxTokens: 1000, // required when a budget applies
promptVersion: "assistant-decide-v1", // Proposed: your own label, shown in the step history
}, async (i, c) => { /* ask the model what the person wants */ });
// The action is an effect step. Its default key is the origin ticket id plus
// the step name, so a retry after a crash sends the same key and cannot
// raise the request twice.
return await ctx.step("raise-it-ticket", plan, {
effect: true, // intent is written before, outcome after
onUnknown: "pause", // crash mid-call: stop and ask a person
probe: async (args) => ({ happened: await helpdesk.exists(args.reference) }), // optional check
}, async (p) => {
return await helpdesk.createTicket(p /* ... */); // your own system, with your own credentials
});
});
// The cap stops the tickets of one conversation or one person from spending
// more than their limit. The ledger records what the effect step did. If a
// crash lands between "about to raise" and "raised", the ticket becomes
// "unknown_outcome" and waits for a person instead of raising it twice. Illustrative only: no customer, no incident and no code stand behind this sketch. The spend cap limits what these tickets spend, and the ledger stops a retry from raising the same request twice. Neither stops a model from being tricked, by text in a message, into asking for an action. A person's approval before the effect step is the only brake qlane offers. Access to the leave system or the help desk is whatever your own credentials allow; qlane does not manage it.