@toist/aja
v0.10.5
Published
Durable Toist host surface: SQLite runtime, HTTP runner, UI mount, filesystem helpers, and the typed runner client.
Readme
@toist/aja
Durable Toist host surface: SQLite runtime, HTTP runner, UI mount, filesystem helpers, and the typed runner client.
import { startRunner } from "@toist/aja"
await startRunner({
rootDir: import.meta.dir,
port: 3000,
})Main exports
startRunner(options)createSqliteRuntime(options)createFilesystemPipelineStore({ rootDir, watch? })loadFilesystemResources(rootDir)createRunnerClient({ baseUrl })- convenience re-exports from
@toist/core
Client quickstart
import { createRunnerClient } from "@toist/aja"
const client = createRunnerClient({ baseUrl: "http://localhost:3000" })
const started = await client.pipelines.runSpec(spec, { name: "world" })
for await (const event of client.runs.events(started.id)) {
console.log(event.type)
}Runtime layout
With startRunner({ rootDir, port }), the runner:
- loads pipelines from
<rootDir>/pipelines - stores runtime state under
<rootDir>/data - reads resources from
<rootDir>/toist.yml - serves UI + API from one Bun process
/manifest
GET /api/manifest returns stable JSON for tooling:
{
"version": 1,
"schema": "https://toist.in/schemas/manifest-v1.json",
"kinds": []
}Resources
loadFilesystemResources(rootDir) reads toist.yml and resolves ${env:VAR} and ${env:VAR:-fallback} substitutions.
Scheduling
A pipeline's schedule: field (a 5-field POSIX cron string) is a declaration, not a binding to any particular firing mechanism. Three paths can deliver scheduled runs; pick the one that matches your deployment.
Recommended: cron + CLI + memory runner
For idempotent pipelines whose effects land in external systems (filesystem, HTTP APIs, Nalich, your DB of choice), the simplest path is your OS scheduler plus a one-shot CLI invocation:
*/5 * * * * cd /path/to/instance && bunx @toist/in run pipelines/digest.yaml --ephemeralEach tick spawns a fresh bun process, loads the YAML, runs the pipeline in a memory-backed runtime, and exits. No long-running runner. No SQLite lock. No daemon to babysit. The same schedule: value lives in the pipeline YAML — the cron entry is generated from it (see toist schedule export).
Pros: familiar tooling, robust to runner crashes (cron just retries on its next tick), zero coordination state to corrupt. Limits: no durable run ledger, no admin-UI visibility, HITL pipelines that suspend mid-run can't resume across ticks.
Embedded scheduler
Useful when you specifically need:
- HITL pipelines that suspend on
human.inputand must resume durably from a persisted ledger - Admin UI visibility of every scheduled run alongside manual launches
- HTTP API surface (other systems calling
/api/pipelines/:id/run) co-located with scheduling
When the runner boots with the durable SQLite runtime, the embedded scheduler scans pipelines for schedule: fields, ticks every 60 s, and fires runs with trigger="scheduled". Best-effort: it fires when the runner is alive and skips occurrences when it is not. There is no missed-run replay, no catch-up window, and no spec snapshotting. When a previous run is pending, running, or suspended, the next cron occurrence is silently skipped, unless the pipeline opts in with concurrency: parallel:
apiVersion: toist.in/v1
id: weekly-digest
schedule: "0 9 * * 1"
concurrency: parallel # optional; default is "skip"
nodes:
- { id: start, kind: start }
- { id: review, kind: human.input, ... }
- { id: out, kind: sink, ... }Pipelines carry references to data (paths, URIs, IDs) — not the data itself — and are reloaded from source at fire time; persisting a snapshot is the data owner's responsibility, not the runner's.
Durable execution (future): Temporal-backed runtime
For exactly-once guarantees, missed-run replay, workflow versioning, complex retry/overlap policies, or multi-host scheduling, run Toist on top of a durable engine such as Temporal. The ToistRuntime interface (runs, tasks, logs, outputs) is designed to be backed by alternative implementations; a Temporal adapter would map runs to workflows, HITL tasks to signals, and the pipeline's schedule field to a native Temporal Schedule. No adapter ships in this package yet — when concrete demand appears, this is the seam.
HITL with cron
HITL pipelines can use the same cron-driven deployment style, but they must use a durable local ledger so the suspend → resume cycle survives process exit. Do not pass --ephemeral for these pipelines.
Minimal scheduled pipeline:
apiVersion: "toist.in/v1"
id: approval-digest
schedule: "*/5 * * * *"
nodes:
- id: start
kind: start
- id: review
kind: human.input
dependsOn: [start]
notify: ["console"]
params:
prompt: Approve this digest?
schema:
type: object
properties:
approved:
type: boolean
- id: out
kind: sink
dependsOn: [review]
input:
value: { expr: "ctx.results.review" }Install a cron entry without --ephemeral:
*/5 * * * * cd /path/to/instance && bunx @toist/in run pipelines/approval-digest.yamlWhen cron fires, the CLI starts the pipeline, creates a durable task, suspends the run, sends the configured notification, and exits. The console notifier prints a resume hint like:
toist tasks answer 42 --response '<json>'List open tasks from the same environment:
toist tasks listRespond with JSON when the human decision is ready:
toist tasks answer 42 --response '{"approved": true}'The embedded scheduler can run the same pipeline; only the firing mechanism differs. In both cases the run must use durable state. HITL pipelines fail loudly when run with --ephemeral; the CLI error names the suspending nodes and points back to this section.
Run status
The runs ledger uses workflow-engine standard status values:
pending— run row created, not yet executingrunning— currently executingsuspended— paused at a HITL task or error reviewsucceeded— completed successfullyfailed— terminated with an error
When to use @toist/aja
Use it when you want a durable single-tenant runner with HTTP APIs. For in-process execution without the server, import @toist/core directly.
