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.