Tide

Your first workflow

Write, run, and replay a durable workflow.

This walkthrough takes you from a freshly installed tide to a running, crash-proof workflow — including a second example that drives ontology state. If you have not installed Tide yet, start with Installation.

1. Write a workflow

Every Tide workflow export defaults an object with two fields:

  • schema — the shape of the input, built with the global S builder. It validates the input before your code runs.
  • main — an async function that receives the validated input and contains your logic.

The file APIs (writeFile, readFile, …), step, log, and S/Infer are all global — no imports needed.

Save this as greet.ts:

const schema = S.object({
  name: S.text(),
  greeting: S.default(S.text(), "Hello"),
});

export default {
  schema,
  main: async (ctx: Infer<typeof schema>) => {
    log(`${ctx.greeting}, ${ctx.name}!`);

    // Group related operations into an atomic, replayable step.
    const receipt = await step("write-greeting", async () => {
      await writeFile("greeting.txt", `${ctx.greeting}, ${ctx.name}!`);
      return { wrote: "greeting.txt" };
    });

    log("step result:", JSON.stringify(receipt));
  },
};

Infer<typeof schema> gives ctx a precise type: name: string (required) and greeting: string (defaulted, so always present in main). A wrong field is a type error in your editor.

2. Run it

tide run greet.ts --input '{"name":"Ada"}'

Tide prints the auto-generated invocation id to stderr, validates the input against the schema, runs main, and journals every operation:

invocation: 7b3f…
Hello, Ada!
step result: {"wrote":"greeting.txt"}

greeting was omitted, so its default ("Hello") was filled in before main ran. To inspect the input shape a workflow accepts, ask for its schema as JSON:

tide schema greet.ts

3. Replay it

Durable execution shows up when you re-run the same invocation id. Give the run an explicit id, then run it again:

tide run greet.ts -i greet-001 --input '{"name":"Ada"}'   # first run — executes live
tide run greet.ts -i greet-001                            # replay — input is remembered

On the second run Tide loads the journal and replays it: the step returns its stored result instantly without re-executing the file write, and the stored input is reused (you do not pass --input again). If the first run had crashed partway through, the replay would pick up exactly where it stopped — no work is repeated, no side effect is doubled. That is the whole point of Tide.

4. Drive ontology state (optional)

The shipped binary embeds Argon, so a workflow can read and write a typed ontology world with ctx.argon. Run this from an Argon project (a directory with an ox.toml); tide run builds the ontology and generates a typed client into .tide/, both in-process.

import { defineWorkflow, S } from "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 }) {
    // World-state writes run at the top level (NOT inside step()): the runtime
    // journals each write and replays it verbatim on resume — exactly-once.
    const receipt = await argon.mutations.hire.receipt({
      employer: Company.ref("#i1"),
      employee: Person.ref("#i2"),
      employer_name: company,
      employee_name: name,
      salary,
    });
    log("hired", name, "— events:", receipt.eventsEmitted.length);

    // Read-your-writes within the run.
    const employees = await argon.queries.all_employees.run({});
    return { hired: name, headcount: employees.length };
  },
});
tide run workflows/onboard.ts --input '{"company":"Acme","name":"Ada","salary":120000}'

The Argon client is fully typed, journaled, and fork-isolated per run. The full story — defineWorkflow, the generated client, determinism rules, durable worlds, and testing — is in Driving Argon.

Next

On this page