Skip to content

Quick Start ​

Run your first resumable job in a single file. No server, no UI — just Node.js.

Install ​

Use Node.js 22+ (the examples in this repository use Node.js 24). For the current libSQL client/dialect combination, apply the client version override before installing.

bash
pnpm add @coji/durably kysely zod @libsql/client @libsql/kysely-libsql

Write a Job ​

Create main.ts:

ts
import { createDurably, defineJob } from '@coji/durably'
import { LibsqlDialect } from '@libsql/kysely-libsql'
import { createClient } from '@libsql/client'
import { z } from 'zod'

// 1. Define a job with steps
const greetJob = defineJob({
  name: 'greet',
  input: z.object({ name: z.string() }),
  output: z.object({ message: z.string() }),
  run: async (step, input) => {
    // Each step is a checkpoint — cached on success
    const greeting = await step.run('build-greeting', () => {
      return `Hello, ${input.name}!`
    })

    await step.run('log-it', () => {
      console.log(greeting)
    })

    return { message: greeting }
  },
})

// 2. Create Durably instance
const client = createClient({ url: 'file:local.db' })
const dialect = new LibsqlDialect({ client })

const durably = createDurably({
  dialect,
  jobs: { greet: greetJob },
})

// 3. Initialize and run
await durably.init()

const { id, output } = await durably.jobs.greet.triggerAndWait({
  name: 'World',
})
console.log(`Run ${id} completed:`, output)
// => Run run_xxx completed: { message: "Hello, World!" }

await durably.stop()
await durably.db.destroy()

Run It ​

bash
npx tsx main.ts

That's it. The job ran, each step was persisted to local.db, and you got the output back.

What Just Happened? ​

  1. defineJob created a job with two steps
  2. createDurably set up the engine with SQLite storage
  3. init() created tables and started the worker
  4. triggerAndWait() queued the job and waited for completion
  5. Each step.run() saved its result — if you run it again with the same idempotency key, cached results are returned instantly

Try Resumability ​

Add a step that fails, then see it resume:

ts
const riskyJob = defineJob({
  name: 'risky',
  input: z.object({}),
  output: z.object({ result: z.string() }),
  run: async (step) => {
    await step.run('step-1', () => {
      console.log('Step 1: running')
      return 'done'
    })

    await step.run('step-2', () => {
      console.log('Step 2: running')
      // Uncomment to simulate crash:
      // throw new Error('boom!')
      return 'done'
    })

    return { result: 'all done' }
  },
})
  1. Run with the error uncommented — step 1 succeeds, step 2 fails
  2. Comment out the error, retrigger the run — a fresh run (new ID) starts with the same input

Next Steps ​

Released under the MIT License.