Driving Argon
Typed, journaled ontology state from a workflow — defineWorkflow and the in-process Argon client.
Overview
A Tide workflow that drives an Argon ontology gets two ergonomic guarantees:
defineWorkflow— a typed authoring entry that gives the workflow body a discoverablectxinstead of ambient globals, with the input schema flowing into the body's first argument.- A typed, in-process Argon client —
ctx.argoncarriesmutations/queries/computesgenerated from the ontology'spubsurface. A wrong field or path is a type error in the editor, not a runtime 400. Every read and write routes through Tide's journaled, replay-deterministic ops — neverfetch— so determinism and fork isolation hold invisibly.
Both are pure sugar over the core export default { schema, main } form; no
runtime protocol changes. The core form keeps working unchanged.
A complete workflow
import { defineWorkflow, S } from "tide";
// The typed client `tide run` / `tide test` generate into `.tide/`.
import { Company, Person, sdk } from "../.tide/argon-client.ts";
export default defineWorkflow({
argon: sdk,
input: S.object({
company: S.text(),
name: S.text(),
salary: S.integer(),
}),
async run({ company, name, salary }, { argon, log }) {
const employer = Company.ref("#i1");
const employee = Person.ref("#i2");
// World-state writes run at the top level, NOT inside step(): the runtime
// journals each write and replays it verbatim on resume (exactly-once), so a
// step() wrapper is unnecessary — and a write inside step() is rejected,
// because a step retry cannot roll back the underlying world-state write.
const receipt = await argon.mutations.hire.receipt({
employer,
employee,
employer_name: company,
employee_name: name,
salary,
});
log("hired", name, "— events:", receipt.eventsEmitted.length);
// Read-your-writes within the run: the query sees the hire.
const employees = await argon.queries.all_employees.run({});
return { hired: name, headcount: employees.length };
},
});Run it from the project root (the directory holding ox.toml):
tide run workflows/onboard.ts --input '{"company":"Acme","name":"Ada","salary":120000}'defineWorkflow
defineWorkflow({
argon?: sdk, // the generated typed client (omit for pure compute)
input?: S.object({ ... }), // input schema; defaults to S.object({})
run(input, ctx) { ... }, // input is typed via Infer<typeof input>
});It returns { schema, main }, so a workflow file still
export defaults the result. run receives:
input— the validated, defaulted input, typed from theinputschema.ctx— the workflow context:
| Field | What it is |
|---|---|
argon | The typed, in-process Argon client (only when an argon SDK was passed; a lazy getter — a workflow that never reads it needs no backend). |
step | Journaled, replayable step runner. |
hitl | Human-in-the-loop wait (hitl.wait(...)); suspends the run for tide resume. |
llm | Large-language-model calls (ai.llm). |
ai | The full ai.* surface (llm, agent, tool, code). |
log, sleep, readFile, writeFile, … | The sandbox capabilities. |
The typed Argon client
tide run and tide test compile the project ontology to a .oxbin and
generate a typed client into .tide/ — both in-process, using the Argon
crates compiled into the tide binary (no external ox toolchain). The output
matches ox gen --target ts / ts-client:
.tide/argon-client.ts— the typed facade (mutations/queries/computes+ entity classes andsdk)..tide/sdk.ts— the dependency-free SDK it imports.
It is regenerated whenever the ontology is rebuilt, so the typed surface always
tracks the live ontology. The directory is build output (gitignored); commit the
.ar sources, not the generated client.
defineWorkflow({ argon: sdk }) calls sdk.use(...) with Tide's in-process
runtime client. The bound client exposes, per pub declaration:
await argon.mutations.hire.run({ ... }); // returns the mutation's result
await argon.mutations.hire.receipt({ ... }); // returns the full receipt
await argon.queries.all_employees.run({}); // returns the typed rowsUse connectArgon(sdk) to bind the client outside a workflow body (for example
in a tide test file):
import { connectArgon } from "tide";
import { sdk } from "../.tide/argon-client.ts";
const argon = connectArgon(sdk);Individual references
Argon individual references are strings of the form #i<n> (#i1, #i2, …).
The generated entity classes give typed constructors:
const employee = Person.ref("#i2");Determinism rules
The typed client is durable and deterministic by construction — you never see ordinals or journals:
- A mutation is journaled. On replay (after a crash or a
tide resume) its recorded receipt is returned verbatim; the mutation never re-runs. - A query within a run reads at the run's pinned transaction watermark, so a
replayed run observes the same data the original run did, even if
mainhas moved on. Reads inside the run also see the run's own writes (read-your-writes). - A run's writes land in a per-run fork and reach
mainonly when the run completes successfully; a failed run's writes are discarded. The fork is durable: a run that suspends (a HITL wait) or crashes before completing reattaches to the same fork on resume, so it reads back its own pre-suspend writes.
Run Argon writes (argon.mutations.*, state.dispatchMutation, …) at the top
level, not inside step(...): the runtime already journals each write and
replays it verbatim on resume, so it is exactly-once without a step() wrapper —
and a write inside step() is rejected, because a step retry cannot roll back
the underlying world-state write (it would double-apply). Reserve step(...) for
non-Argon side effects (an HTTP call, a file write you depend on) so those,
too, are journaled and replay-stable.
mainis durable across invocations: the embedded engine persistsmain's event log under<project>/.tide/worldand replays it on open, so a run's promoted writes are read by every latertide run. Seed a world from one workflow, then read those pre-existing instances from another.--world-dir <dir>roots the world elsewhere;--freshwipes it.tide testruns against a throwaway store, so tests never read or pollute the persistent world.
Testing locally
tide test discovers *.test.ts / *.spec.ts, builds the ontology, and runs
each test case against the real Argon reasoner in its own per-run fork over
an ephemeral world store — one case's writes never leak into the next, never
reach main, and never touch the persistent .tide/world, so a suite is
order-independent and deterministic regardless of accumulated world state:
import { describe, expect, test } from "tide:test";
import { connectArgon } from "tide";
import { Company, Person, sdk } from "../.tide/argon-client.ts";
const argon = connectArgon(sdk);
describe("employment onboarding", () => {
test("hire records a person", async () => {
const receipt = await argon.mutations.hire.receipt({
employer: Company.ref("#i1"),
employee: Person.ref("#i2"),
employer_name: "Acme",
employee_name: "Ada",
salary: 120000,
});
expect(receipt.eventsEmitted.length > 0).to.equal(true);
const employees = await argon.queries.all_employees.run({});
expect(employees.length).to.equal(1);
});
});tide test # human-readable
tide test --json # structured results (for tooling / agents)For coding agents
The workflow manifest and the test output are structured for programmatic use:
- Workflow shape. A workflow module
export defaults{ schema, main }(whatdefineWorkflowreturns).tide schema <file>prints the inputschemaas JSON. - Typed surface discovery. After a build,
.tide/argon-client.tsenumerates every callable:sdk.mutations.*,sdk.queries.*,sdk.computes.*, with parameter and result types. Read it to discover the exact ontology surface. - Structured test output.
tide test --jsonemits{ status, suites: [{ filePath, status, cases: [{ name, fullName, status, error }], output, error }] }.statusis"passed"/"failed";error.messagecarries the assertion failure.