From 11cac5cea5c40741608c04b9368a55931581c460 Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 10 Sep 2026 22:42:30 +0200 Subject: [PATCH 01/15] Finish Relayflows product docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The content/docs/relayflows/*.mdx files existed with correct front-matter but bodies copy-pasted from the agents product docs (cloud personas, granola-prospect, npx agentworkforce deploy) — none of it described the flow engine. Rewrites all four, grounded in the actual flows repo (AgentWorkforce/flows): the RFC-0001 step ladder, the real hello flow and hello-agent.flow.yaml examples, the real Ctx authoring interface, the real CLI usage string from packages/sdk/src/cli.ts, and a real captured run transcript (docs/evidence/ws13/agent-run.txt) instead of a mocked one. Follows factory's tone (short declarative sentences, a numbered core loop, real command blocks) and file's (Relayfile) content grouping. Wires the section into the site the same way factory is wired in: - registers `relayflowsSection` in lib/product-docs-nav.ts (repo AgentWorkforce/flows; no version badge yet, matching `agents`, since the CLI and scaffolder aren't published to npm) - re-exports it from lib/product-docs.ts - adds app/docs/relayflows/{page.tsx,[slug]/page.tsx,[slug]/og.png/route.tsx, markdown/[slug]/route.ts}, copied from factory's equivalents - adds a Workflow icon to the product switcher in DocsNav.tsx The CLI/scaffolder aren't released yet, so introduction.mdx and quickstart.mdx both carry an upfront Note saying so, worded the same way the flows repo's own README already states it — rather than writing a quickstart that 404s on the first command. Verified: - npx vitest run lib/test/product-docs.test.ts -> Test Files 1 passed (1), Tests 5 passed (5) - npx tsc --noEmit -> clean - npm run build -> succeeds; emits /docs/relayflows/{introduction,quickstart,build,cli}, their og.png variants, and their markdown/*.md mirrors - npx vitest run (full suite) -> Test Files 11 passed (11), Tests 32 passed (32) Co-authored-by: Cursor --- .../docs/relayflows/[slug]/og.png/route.tsx | 42 +++++++ web/app/docs/relayflows/[slug]/page.tsx | 59 ++++++++++ .../docs/relayflows/markdown/[slug]/route.ts | 30 +++++ web/app/docs/relayflows/page.tsx | 5 + web/components/docs/DocsNav.tsx | 2 + web/content/docs/relayflows/build.mdx | 111 ++++++++++++++++++ web/content/docs/relayflows/cli.mdx | 66 +++++++++++ web/content/docs/relayflows/introduction.mdx | 111 ++++++++++++++++++ web/content/docs/relayflows/quickstart.mdx | 71 +++++++++++ web/lib/product-docs-nav.ts | 25 ++++ web/lib/product-docs.ts | 1 + 11 files changed, 523 insertions(+) create mode 100644 web/app/docs/relayflows/[slug]/og.png/route.tsx create mode 100644 web/app/docs/relayflows/[slug]/page.tsx create mode 100644 web/app/docs/relayflows/markdown/[slug]/route.ts create mode 100644 web/app/docs/relayflows/page.tsx create mode 100644 web/content/docs/relayflows/build.mdx create mode 100644 web/content/docs/relayflows/cli.mdx create mode 100644 web/content/docs/relayflows/introduction.mdx create mode 100644 web/content/docs/relayflows/quickstart.mdx diff --git a/web/app/docs/relayflows/[slug]/og.png/route.tsx b/web/app/docs/relayflows/[slug]/og.png/route.tsx new file mode 100644 index 00000000..acdb29d8 --- /dev/null +++ b/web/app/docs/relayflows/[slug]/og.png/route.tsx @@ -0,0 +1,42 @@ +import { ImageResponse } from 'next/og'; +import { notFound } from 'next/navigation'; + +import { DefaultVariant, loadBrandFonts, OG_SIZE } from '../../../../../lib/og/template'; +import { getProductDoc, getProductDocSlugs } from '../../../../../lib/product-docs'; +import { relayflowsSection } from '../../../../../lib/product-docs-nav'; + +export const runtime = 'nodejs'; +export const dynamic = 'force-static'; + +type RouteContext = { + params: Promise<{ slug: string }>; +}; + +export function generateStaticParams() { + return getProductDocSlugs(relayflowsSection).map((slug) => ({ slug })); +} + +export async function GET(_request: Request, { params }: RouteContext) { + const { slug } = await params; + const doc = getProductDoc(relayflowsSection.id, slug); + + if (!doc) { + notFound(); + } + + const { fonts, headingFamily, bodyFamily } = await loadBrandFonts(); + + return new ImageResponse( + , + { + ...OG_SIZE, + ...(fonts.length > 0 ? { fonts } : {}), + } + ); +} diff --git a/web/app/docs/relayflows/[slug]/page.tsx b/web/app/docs/relayflows/[slug]/page.tsx new file mode 100644 index 00000000..7b672b5d --- /dev/null +++ b/web/app/docs/relayflows/[slug]/page.tsx @@ -0,0 +1,59 @@ +import type { Metadata } from 'next'; + +import { ProductDocPage } from '../../../../components/docs/ProductDocPage'; +import { ogImage } from '../../../../lib/og-meta'; +import { + relayflowsSection, + getProductDoc, + getProductDocMarkdownUrl, + getProductDocSlugs, +} from '../../../../lib/product-docs'; +import { absoluteUrl } from '../../../../lib/site'; + +const SECTION_ID = relayflowsSection.id; + +type PageProps = { + params: Promise<{ slug: string }>; +}; + +export function generateStaticParams() { + return getProductDocSlugs(relayflowsSection).map((slug) => ({ slug })); +} + +export async function generateMetadata({ params }: PageProps): Promise { + const { slug } = await params; + const doc = getProductDoc(SECTION_ID, slug); + + if (!doc) { + return { title: 'Not Found' }; + } + + const ogPath = `/docs/${SECTION_ID}/${slug}/og.png`; + + return { + title: `${doc.frontmatter.title} — Relayflows`, + description: doc.frontmatter.description, + alternates: { + canonical: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), + types: { 'text/markdown': getProductDocMarkdownUrl(SECTION_ID, slug) }, + }, + openGraph: { + title: `${doc.frontmatter.title} — Relayflows`, + description: doc.frontmatter.description, + url: absoluteUrl(`/docs/${SECTION_ID}/${slug}`), + type: 'article', + images: [ogImage(ogPath, `${doc.frontmatter.title} — Relayflows docs`)], + }, + twitter: { + card: 'summary_large_image', + title: `${doc.frontmatter.title} — Relayflows`, + description: doc.frontmatter.description, + images: [absoluteUrl(ogPath)], + }, + }; +} + +export default async function RelayflowsDocPage({ params }: PageProps) { + const { slug } = await params; + return ; +} diff --git a/web/app/docs/relayflows/markdown/[slug]/route.ts b/web/app/docs/relayflows/markdown/[slug]/route.ts new file mode 100644 index 00000000..a9bd1a4b --- /dev/null +++ b/web/app/docs/relayflows/markdown/[slug]/route.ts @@ -0,0 +1,30 @@ +import { relayflowsSection, getProductDocMarkdown, getProductDocSlugs } from '../../../../../lib/product-docs'; + +export const revalidate = 86400; +export const dynamicParams = false; + +export function generateStaticParams() { + return getProductDocSlugs(relayflowsSection).map((slug) => ({ slug: `${slug}.md` })); +} + +type RouteProps = { + params: Promise<{ slug: string }>; +}; + +export async function GET(_request: Request, { params }: RouteProps) { + const { slug: rawSlug } = await params; + const slug = rawSlug.replace(/\.md$/, ''); + + const doc = getProductDocMarkdown(relayflowsSection.id, slug); + + if (!doc) { + return new Response('Not found', { status: 404 }); + } + + return new Response(doc.markdown, { + headers: { + 'Content-Type': 'text/markdown; charset=utf-8', + 'Cache-Control': 'public, max-age=86400', + }, + }); +} diff --git a/web/app/docs/relayflows/page.tsx b/web/app/docs/relayflows/page.tsx new file mode 100644 index 00000000..461a14c6 --- /dev/null +++ b/web/app/docs/relayflows/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from 'next/navigation'; + +export default function RelayflowsDocsIndexPage() { + redirect('/docs/relayflows/introduction'); +} diff --git a/web/components/docs/DocsNav.tsx b/web/components/docs/DocsNav.tsx index 31d42125..1d6042d4 100644 --- a/web/components/docs/DocsNav.tsx +++ b/web/components/docs/DocsNav.tsx @@ -25,6 +25,7 @@ import { Smile, Terminal, Users, + Workflow, Zap, } from 'lucide-react'; import { BsChatRightText } from 'react-icons/bs'; @@ -53,6 +54,7 @@ const productSectionIcons: Record = { agents: Bot, file: FolderOpenIcon, loop: Repeat, + relayflows: Workflow, }; const currentRelayDocsVersion = docsVersions.find((version) => version.id === currentDocsVersion)?.shortLabel; diff --git a/web/content/docs/relayflows/build.mdx b/web/content/docs/relayflows/build.mdx new file mode 100644 index 00000000..3660b1c4 --- /dev/null +++ b/web/content/docs/relayflows/build.mdx @@ -0,0 +1,111 @@ +--- +title: 'Build a flow' +description: 'Write your own steps, verification, and permissions — TypeScript or YAML, same journal underneath.' +--- + +Start with the job in one sentence: what runs, in what order, and what proves each step actually did its job. If a step needs a check you can't state yet, that's the sign it needs one before you wire it up, not after. + +## Two ways to author the same thing + +**YAML** is the deterministic-DAG path: steps, dependencies, verification, and permissions as data. It's what a `flows check` or a CI gate reads without executing anything. + +```yaml +version: '0.1.0' +name: hello-agent +steps: + - id: greet + type: deterministic + command: 'printf hello' + verification: + type: output_contains + value: hello + + - id: edit + type: agent + dependsOn: [greet] + instruction: 'Produce the hello artifact.' + recoveryMode: reset + maxIterations: 2 + surfaces: + workspace: + - surface: repo + streams: + - stream: agent-notes + permissions: + fileGlobs: ['artifacts/**'] + accessPreset: readwrite + verification: + type: output_contains + value: agent-ok + + - id: finish + type: deterministic + dependsOn: [edit] + command: 'printf done' + +budget: + maxTokensIn: 64 + maxTokensOut: 32 + maxDollars: '0.02' +``` + +**TypeScript** is the programmatic path — the same primitives, called imperatively: + +```ts +import { flow } from '@relayflows/surface'; + +export default flow('release-note', async (f) => { + const diff = await f.run('git diff main'); + const note = await f.llm`One-line release note for: ${diff}`; + const ok = await f.human(`Ship this note?\n${note}`, { to: 'khaliq' }); + if (!ok) return f.done('canceled'); + f.done('success'); +}); +``` + +Both compile to the same journal protocol. Reach for YAML when the shape is a fixed DAG you want readable at a glance; reach for TypeScript when a step's next move depends on what a previous one returned. + +## The context a flow body gets + +```ts +interface Ctx { + run(command: string): Step; + llm(strings: TemplateStringsArray, ...values: unknown[]): Step; + agent(name: string, options: { task: string; workspace?: string }): Step<{ summary: string; artifacts: string[] }>; + human(question: string, options: { to: string }): Promise; + dispatch(flow: string, input: unknown): Promise; + done(reason: 'success' | 'step_failed' | 'canceled' | 'budget_exceeded'): void; +} +``` + +That's the whole kernel-level vocabulary a step body speaks: `run`, `llm`, `agent` for work; `human`, `dispatch`, `done` for control. `human` parks the run on a durable await — it isn't a blocking call that ties up a process, it's a wait the journal can survive a restart across. `dispatch` hands work to a named child flow and returns its typed result. + +## Verification, not vibes + +Every step's exit code is checked automatically. On top of that: + +- **`output_contains`** — an opt-in string match against the step's output. Fast to write, good for a smoke check. +- **`json_schema`** on an `llm` step — the model's reply must parse and validate against the schema before anything downstream sees it. + +A step that fails its check doesn't get to report success. It's `verification_failed`, journaled with exactly which check failed — never a silent pass because the agent said it was fine. + +## Permissions and recovery + +An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit: + +- **`reset`** (the default) — the next attempt starts from the pinned workspace revision. Clean slate, no half-finished edit left lying around. +- **`inspect`** — the next attempt starts inside the dirty workspace, with the failed attempt's trajectory tail injected as context, and decides whether to continue or redo. +- **`manual`** — parks the run as `needs_human` with a diff of the pinned revision against whatever's actually there. + +`maxIterations` bounds how many attempts a step gets before that decision is made for it. + +## Next + + + + Scaffold one and run it end to end. + + + check, run, resume — every flag. + + diff --git a/web/content/docs/relayflows/cli.mdx b/web/content/docs/relayflows/cli.mdx new file mode 100644 index 00000000..12051e41 --- /dev/null +++ b/web/content/docs/relayflows/cli.mdx @@ -0,0 +1,66 @@ +--- +title: 'CLI' +description: 'Check a spec, run it, resume it, watch it — the flows command surface.' +--- + +```text +flows check [--json] +flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir ] +flows run --cloud [--json] [--wait] +flows run [--json] [--no-spawn] [--no-observer-link] [--data-dir ] [--local-agent] --input +flows resume [--json] [--no-spawn] [--no-observer-link] [--data-dir ] +flows tick start --schedule-id --interval-ms [--epoch-ms ] [--max-catch-up ] [--poll-interval-ms ] [--data-dir ] +flows hn-monitor start [--data-dir ] [--poll-interval-ms ] +flows observer [--data-dir ] +``` + +## Check + +```bash +flows check my-flow.flow.ts +``` + +Validates the spec — schema, step graph, verification blocks, and whether every declared CLI actually exists and is authenticated. Nothing runs; nothing is spawned. This is the same preflight a run does before its first step, exposed on its own so a broken spec fails in CI, not at minute 27 of a real run. + +## Run + +```bash +flows run my-flow.flow.ts --local-agent --input '{}' # TypeScript flow, local CLI +flows run workflow.yaml # YAML/spec.json flow +flows run --cloud --wait workflow.yaml # dispatch to the hosted engine, block for the result +``` + +`--local-agent` attaches the existing SDK agent worker to your local daemon and runs the declared CLI (`claude`, `codex`, …) with its own local access. It's what makes an authored TypeScript flow's `agent` steps runnable without a separate credential path. + +By default `run` spawns a daemon for the run if one isn't already up. `--no-spawn` (or `FLOWS_NO_SPAWN=1` for a whole environment) asserts a daemon is already present instead — the lever CI uses to fail loudly on a missing daemon rather than silently start one. + +`--data-dir ` points at the journal's storage directory; it defaults to `.relayflowd` in the current project. + +## Resume + +```bash +flows resume +``` + +Picks a run back up from its journal. Completed steps aren't re-executed; only the work that never finished, or never got a recorded outcome, runs again. + +## Observer links + +```bash +flows observer +``` + +Mints a read-only link for watching a run's activity in real time. Minting is best-effort — a failed mint never fails the run — so `--no-observer-link` is there for anyone who wants to skip it outright (CI, for instance). + +## Triggers + +```bash +flows tick start --schedule-id daily --interval-ms 86400000 spec.json +flows hn-monitor start spec.json +``` + +`tick` is the general-purpose durable schedule: a deterministic-id claim on each interval, with catch-up bounded by `--max-catch-up` if the process was down when an interval fired. `hn-monitor` is a named, narrower proactive trigger built the same way. Both are the same idea underneath: a schedule that reports whether it's still alive, not one that silently stops firing for weeks and reports nothing. + +## `--json` + +Every command accepts `--json` for structured output instead of the human-readable progress line — the shape a CI step or another program should read, not the terminal renderer. diff --git a/web/content/docs/relayflows/introduction.mdx b/web/content/docs/relayflows/introduction.mdx new file mode 100644 index 00000000..21f4eb87 --- /dev/null +++ b/web/content/docs/relayflows/introduction.mdx @@ -0,0 +1,111 @@ +--- +title: 'Relayflows' +description: 'A deterministic script over agentic primitives — steps you can inspect, verify, and resume, not a prompt and a hope.' +--- + +Relayflows turns a coding-agent task into steps you can inspect and verify. A flow combines shell commands, model calls, and coding agents with a journal that records what each step did and why it completed. Start with a small local flow; add verification as the task grows. + +Every effect is journaled before it's real. If a step's write to that journal fails, the step fails — there's no silent fallback and no "it probably worked." That's the trade Relayflows makes everywhere: less magic, more evidence. + +## The ladder + +Every flow is built from the same small set of rungs, and you only climb as high as the task needs: + +1. **`run`** — a shell command. No model anywhere. +2. **`llm`** — a bare model call. Prompt in, verified output out — no workspace, no tool use. +3. **`agent`** — a harnessed coding agent in a workspace. Returns an artifact, not just text. +4. **`human` / `dispatch` / `done`** — durable approval, handing work to a child flow, and a typed finish. + +Most flows on day one are just `run` and `llm`. `agent` is the rung you climb to when the step needs hands on a real workspace. + +## A whole flow + +```ts +import { flow } from '@relayflows/surface'; + +export default flow('hello', async (f) => { + const greeting = await f.run('echo "Hello from Relayflows"'); + console.log(greeting.trim()); + + const answer = await f.agent('greeter', { + task: 'Reply with one short hello sentence. Do not use tools or modify files.', + }); + console.log(answer.summary); + + f.done('success'); +}); +``` + +`f.run` executes a shell command and returns its output. `f.agent` hands a task to a real coding-agent CLI in a workspace and returns `{ summary, artifacts }` — not a raw transcript. `f.done` closes the run with one reason from a closed set: `success`, `step_failed`, `canceled`, or `budget_exceeded`. There's no fifth option a caller has to guess about. + +## Verification instead of vibes + +An agent's failure mode is wrong output, not no output — a retry built for "it crashed" doesn't catch "it returned something plausible and wrong." So a step's completion isn't the agent's opinion; it's a check the kernel runs on what came back. + +```yaml +version: '0.1.0' +name: hello-agent +steps: + - id: greet + type: deterministic + command: 'printf hello' + verification: + type: output_contains + value: hello + + - id: edit + type: agent + dependsOn: [greet] + instruction: 'Produce the hello artifact.' + recoveryMode: reset + maxIterations: 2 + permissions: + fileGlobs: ['artifacts/**'] + accessPreset: readwrite + verification: + type: output_contains + value: agent-ok +``` + +Every step's exit code is checked automatically; `output_contains` is an opt-in accelerator on top of it, and an `llm` step can require its output to match a `json_schema` instead. A step that fails verification doesn't get to report success — it's `verification_failed`, journaled, visible. + +`recoveryMode: reset` is the agent step's crash contract: if this step dies mid-edit, the next attempt starts from the pinned workspace revision, not from whatever half-finished state was left behind. `permissions` bounds what that attempt can touch while it runs. + +## Resumable by construction + +Kill the process mid-run and start it again — completed steps aren't re-executed, and their effects aren't re-sent: + +```text +$ npx --no-install flows run hello.flow.ts --local-agent --input '{}' +○ run-1 (deterministic) 0.00s +✓ run-1 (deterministic) 1.02s completionReason: success +Hello from Relayflows +○ agent-2 (agent) [agent: preparing] 0.00s +↻ agent-2 (agent) [agent: running] 12.51s +✓ agent-2 (agent) [agent: completed] 28.95s completionReason: success +Hello! How can I help you today? + +RUN 01M20JYM4T9FNGNFX4NQCK34JK completed (3 steps) completionReason: success +``` + +That's a real captured run, not a mockup — every step carries a `completionReason` from a closed vocabulary (`success`, `verification_failed`, `retries_exhausted`, `lease_expired`, `crashed`, `timeout`, `worker_error`, `budget_exceeded`, `canceled`), so a run's journal always says which of those it was. No raw stack trace ever stands in for one. + + + The CLI and scaffolder aren't published to npm yet — the commands on this + page are the intended entry point. Until they land, build the SDK from + source to try a flow locally. + + +## Start here + + + + From nothing to a resumed run, five steps. + + + TypeScript or YAML — the same journal underneath. + + + check, run, resume, observe. + + diff --git a/web/content/docs/relayflows/quickstart.mdx b/web/content/docs/relayflows/quickstart.mdx new file mode 100644 index 00000000..cfc8fe93 --- /dev/null +++ b/web/content/docs/relayflows/quickstart.mdx @@ -0,0 +1,71 @@ +--- +title: 'Quickstart' +description: 'From nothing to a resumed, verified run — five steps, no model credential needed until step 4.' +--- + +Follow these in order. Steps 1–3 touch nothing that costs money; step 4 makes a real model call. + +## 1. Install + +```bash +npm install -g relayflows +``` + + + Not published yet — `relayflows` and `create-flow` are pre-release. Until + they land on npm, clone the SDK and build it locally to run the commands + below against your own checkout. + + +## 2. Scaffold a flow + +```bash +npx create-flow@latest my-flow +cd my-flow +npm install +``` + +This writes the flow file, a `flows.json` project config, and an npm project with its dependencies already declared. Pass `--cli codex` to target Codex instead of Claude, or `--template deterministic` for a starter that makes no model call at all — useful if you don't have an agent CLI authenticated yet. + +## 3. Check it before running it + +```bash +npx flows check my-flow.flow.ts +``` + +Validates the spec — schema, step graph, verification blocks — without spawning a shell command or a model. This is the same check a CI gate would run. + +## 4. Run it + +```bash +npm start +``` + +Which runs the generated command directly: + +```bash +flows run my-flow.flow.ts --local-agent --input '{}' +``` + +`--local-agent` attaches the existing SDK agent worker to your local daemon and runs your authenticated CLI with its own access — no separate credential to configure. Requires Node 22.18+ and a signed-in `claude` or `codex` on `PATH`. + +## 5. Kill it and resume it + +Interrupt the process mid-run (`Ctrl-C`, or `kill -9` the daemon) and pick it back up: + +```bash +npx flows resume +``` + +Steps the journal already recorded as complete don't run again — you'll see the run pick up exactly where it stopped, not from the top. + +## Next + + + + Write your own steps, verification, and permissions. + + + Every command: check, run, resume, tick, observe. + + diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index 1b947f82..c70f5d63 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -211,11 +211,36 @@ export const factorySection: ProductDocSection = { ], }; +export const relayflowsSection: ProductDocSection = { + id: 'relayflows', + label: 'Relayflows', + tagline: 'Deterministic scripts over agentic primitives — every step verified, every crash resumable.', + repo: 'AgentWorkforce/flows', + nav: [ + { + title: 'Start', + items: [ + { title: 'Introduction', slug: 'introduction' }, + { title: 'Quickstart', slug: 'quickstart' }, + ], + }, + { + title: 'Author', + items: [{ title: 'Build a flow', slug: 'build' }], + }, + { + title: 'Run', + items: [{ title: 'CLI', slug: 'cli' }], + }, + ], +}; + export const productSections: ProductDocSection[] = [ fileSection, agentsSection, factorySection, loopSection, + relayflowsSection, ]; export function getProductSection(id: string): ProductDocSection | null { diff --git a/web/lib/product-docs.ts b/web/lib/product-docs.ts index 27cdd562..aba5007b 100644 --- a/web/lib/product-docs.ts +++ b/web/lib/product-docs.ts @@ -17,6 +17,7 @@ export { factorySection, fileSection, loopSection, + relayflowsSection, productSections, getProductSection, getProductSectionForPath, From fd903fe6b8a7078341d24a1992660b297527a97b Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 10 Sep 2026 22:51:30 +0200 Subject: [PATCH 02/15] docs(relayflows): frictionless quickstart + multi-agent, cloud, memory, reliability pages - introduction/quickstart: drop the npm-publication hedge notes, tighten quickstart to a confident numbered flow matching the loop precedent - add four new pages grounded in the flows repo's real surface and specs: - multi-agent.mdx: named agents with per-step cli/model (shipped compiler feature), f.human approval, f.dispatch to a child flow - cloud.mdx: flows run --cloud / runInCloud, accepted-vs-completed, current limits (YAML/JSON only, one-hour ceiling) - memory-and-integrations.mdx: f.memory (relayhistory-backed, gate 5) and generated relayfile helpers (gate 6), with an honest note on the current stub memory provider - reliability.mdx: exit code contract, closed completionReason vocabulary, crash/resume/recoveryMode, exactly-once effects, the authored-operation lifecycle rules - wire all four into a new 'Going further' nav group Verified: npx tsc --noEmit (clean), npx vitest run (11 files, 32 passed), npm run build (exit 0, all /docs/relayflows/* pages including the four new ones render as SSG with og.png + markdown mirrors). Co-authored-by: Cursor --- web/content/docs/relayflows/cloud.mdx | 50 +++++++++++++ web/content/docs/relayflows/introduction.mdx | 12 ++- .../relayflows/memory-and-integrations.mdx | 62 ++++++++++++++++ web/content/docs/relayflows/multi-agent.mdx | 73 +++++++++++++++++++ web/content/docs/relayflows/quickstart.mdx | 47 ++++++------ web/content/docs/relayflows/reliability.mdx | 65 +++++++++++++++++ web/lib/product-docs-nav.ts | 9 +++ 7 files changed, 289 insertions(+), 29 deletions(-) create mode 100644 web/content/docs/relayflows/cloud.mdx create mode 100644 web/content/docs/relayflows/memory-and-integrations.mdx create mode 100644 web/content/docs/relayflows/multi-agent.mdx create mode 100644 web/content/docs/relayflows/reliability.mdx diff --git a/web/content/docs/relayflows/cloud.mdx b/web/content/docs/relayflows/cloud.mdx new file mode 100644 index 00000000..740887c1 --- /dev/null +++ b/web/content/docs/relayflows/cloud.mdx @@ -0,0 +1,50 @@ +--- +title: 'Cloud' +description: 'Run a flow on hosted infrastructure instead of your own machine — the same verification, the same journal.' +--- + +A flow doesn't need your laptop up to run. `flows run --cloud` submits the exact same spec to hosted infrastructure — the Rust runtime still executes and verifies every step; only where it runs changes. + +## Run it + +```bash +flows run --cloud examples/ship-feature.flow.yaml +flows run --cloud --wait --json examples/ship-feature.flow.yaml +``` + +Without `--wait`, exit `0` means the run was **accepted**, not completed — you get a run ID back and the run continues on its own. With `--wait`, exit `0` means Cloud reported the run `completed` with a validated `success` reason. A failed or cancelled run, or an observation failure, exits `1`. + +## From the SDK + +```ts +import { runInCloud, waitForCloudFlowRun } from '@relayflows/sdk'; + +const accepted = await runInCloud( + { path: './flow.yaml' }, + { token: process.env.FLOWS_CLOUD_TOKEN } +); +console.log(accepted.runId); // accepted, not completed + +const finished = await waitForCloudFlowRun(accepted.runId); +console.log(finished.status, finished.completionReason); +``` + +Set `FLOWS_CLOUD_TOKEN` to a Cloud API token authorized for `workflow:invoke:write` and `workflow:runs:read`. `FLOWS_CLOUD_URL` points at a different Cloud deployment if you're not using the default. + +## What's different about a cloud run + +- **Only declarative flows.** `--cloud` accepts `flow.yaml` / `spec.json`, not an authored `.flow.ts` file — the SDK refuses those before making an HTTP call rather than uploading code that can't run there. +- **Accepted isn't completed.** An interruption after submission but before the acceptance receipt reports `admission_unknown` — the server may already have started a non-idempotent run. Don't resubmit blindly; check the run ID you already have first. +- **One-hour execution ceiling.** Cloud's current executor has a one-hour deadline per run, independent of any local timeout you'd otherwise configure. +- **Step-level journal evidence stays local to this API.** The completion reason you get back is validated against the same closed vocabulary as a local run, but per-step journal detail isn't exposed through this endpoint yet. + +## Next + + + + Named agents, different CLIs, one spec. + + + What a completion reason actually guarantees. + + diff --git a/web/content/docs/relayflows/introduction.mdx b/web/content/docs/relayflows/introduction.mdx index 21f4eb87..ce13c7ed 100644 --- a/web/content/docs/relayflows/introduction.mdx +++ b/web/content/docs/relayflows/introduction.mdx @@ -90,17 +90,11 @@ RUN 01M20JYM4T9FNGNFX4NQCK34JK completed (3 steps) completionReason: success That's a real captured run, not a mockup — every step carries a `completionReason` from a closed vocabulary (`success`, `verification_failed`, `retries_exhausted`, `lease_expired`, `crashed`, `timeout`, `worker_error`, `budget_exceeded`, `canceled`), so a run's journal always says which of those it was. No raw stack trace ever stands in for one. - - The CLI and scaffolder aren't published to npm yet — the commands on this - page are the intended entry point. Until they land, build the SDK from - source to try a flow locally. - - ## Start here - From nothing to a resumed run, five steps. + From nothing to a resumed run in a few commands. TypeScript or YAML — the same journal underneath. @@ -109,3 +103,7 @@ That's a real captured run, not a mockup — every step carries a `completionRea check, run, resume, observe. + + + See how far the ladder goes: named agents, cloud dispatch, memory, and integrations. + diff --git a/web/content/docs/relayflows/memory-and-integrations.mdx b/web/content/docs/relayflows/memory-and-integrations.mdx new file mode 100644 index 00000000..c7ca2168 --- /dev/null +++ b/web/content/docs/relayflows/memory-and-integrations.mdx @@ -0,0 +1,62 @@ +--- +title: 'Memory & integrations' +description: 'Relayhistory-backed memory and Relayfile-backed integrations — the two substrates a flow reaches without hand-rolling either.' +--- + +A flow doesn't carry its own database or its own Slack client. Two existing systems back it instead: [Relayloop](/docs/loop) for memory across runs, and [Relayfile](/docs/file) for everything a flow reads from or writes to outside itself. + +## Memory: recall, cite, learn + +A step declares what it needs from prior runs, and the pack that comes back is charged to that step's own budget — itemized, in that step's journal entry, never a shared pool: + +```ts +export default flow('triage', async (f) => { + const priorArt = await f.memory.recall('similar bugs in the billing module'); + const why = await f.memory.why('fix the billing race condition'); + + const fix = await f.agent('fixer', { + task: `Fix the race condition.\n\nRelevant history:\n${priorArt}`, + }); + + await f.memory.learn(`Root cause: ${fix.summary}`); + f.done('success'); +}); +``` + +Under the hood this is [Relayloop](/docs/loop)'s `ai-hist pack` — the same searchable, local-first history your team already builds by using Claude Code, Codex, and Agent Relay. A flow's memory queries land in the same corpus as everyone's terminal sessions, so a fix an engineer already worked out by hand is something an agent step can cite, not rediscover. + + + Retrieval quality is the acceptance bar here, not the wiring: the target is + an agent visibly avoiding a mistake a prior run already made, citation + included. Today's shipped provider is a fixed stub while real `ai-hist + pack` retrieval lands — the `memory.injected` journal entry and its + per-step budget accounting are real regardless of which provider answers + the query. + + +## Integrations: a SaaS is a directory, not an API client + +Reaching Slack, GitHub, or Linear from a flow doesn't mean writing a provider SDK call and hoping the token scope is right. [Relayfile](/docs/file) mounts each provider as a directory a flow reads and writes: + +```ts +export default flow('release-note', async (f) => { + const diff = await f.run('git diff main'); + const note = await f.llm`One-line release note for: ${diff}`; + + const receipt = await f.slack.post('#releases', note); + f.done('success'); +}); +``` + +`f.slack`, `f.github`, `f.linear`, and every other generated helper compile to a mount write against Relayfile's [adapter catalog](/docs/file/adapters-and-providers) — the same 50-provider surface Relayfile ships today, projected into flow-native verbs. The receipt a helper returns *is* the journaled effect record: post the same Slack message twice on resume and the mount write dedupes it, so a retried step can't double-send. + +## Next + + + + The full integration catalog a flow can reach. + + + How an effect stays exactly-once across a crash. + + diff --git a/web/content/docs/relayflows/multi-agent.mdx b/web/content/docs/relayflows/multi-agent.mdx new file mode 100644 index 00000000..d3c74b2e --- /dev/null +++ b/web/content/docs/relayflows/multi-agent.mdx @@ -0,0 +1,73 @@ +--- +title: 'Multi-agent flows' +description: 'Several named agents, different CLIs and models, one flow — plus durable human approval and handoff to child flows.' +--- + +A flow isn't locked to one CLI or one model. Name as many agents as the task needs, give each one its own `cli` and `model`, and assign them to steps — the planning step can run on Opus, the edit can go to Codex, and the review can come back to Claude, all in one run, each step's identity and cost accounted separately. + +## Named agents + +```yaml +name: ship-feature +agents: + planner: { cli: claude, model: claude-opus-4-6 } + implementer: { cli: codex, model: gpt-5.6-codex } + reviewer: { cli: claude, model: claude-sonnet-4-6 } + +steps: + - id: plan + type: agent + agent: planner + instruction: 'Plan the implementation for: {{input.task}}' + + - id: implement + type: agent + agent: implementer + dependsOn: [plan] + instruction: 'Implement the plan from the previous step.' + + - id: review + type: agent + agent: reviewer + dependsOn: [implement] + instruction: 'Review the diff for correctness and security.' + verification: + type: output_contains + value: 'APPROVED' +``` + +Each step's `agent:` selector resolves to that named `{ cli, model }` pair at compile time — the kernel never guesses a default. An unused or step-shadowed named agent is linted by `flows check`, so a stale declaration doesn't quietly rot in the spec. + +## Asking a human, then handing off + +```ts +import { flow } from '@relayflows/surface'; + +export default flow('ship-feature', async (f) => { + const plan = await f.agent('planner', { + task: 'Plan implementation for: add OAuth2 support', + }); + + const ok = await f.human(`Ship this?\n${plan.summary}`, { to: 'khaliq' }); + if (!ok) return f.done('canceled'); + + const pr = await f.dispatch('garden/implement', plan); + f.done('success'); +}); +``` + +`f.human` parks the run on a durable wait — nothing sits there blocking a thread, and the wait survives a restart exactly like a crash mid-step does. `f.dispatch` hands the plan to a named child flow and returns its typed result, so one large flow decomposes into several smaller ones instead of a script that tries to do everything. + +## Next + + + + Run the same flow on hosted infrastructure. + + + Recall past runs; reach Slack, GitHub, and Linear. + + + The crash-resume contract and the closed failure vocabulary. + + diff --git a/web/content/docs/relayflows/quickstart.mdx b/web/content/docs/relayflows/quickstart.mdx index cfc8fe93..d9e8431f 100644 --- a/web/content/docs/relayflows/quickstart.mdx +++ b/web/content/docs/relayflows/quickstart.mdx @@ -1,9 +1,9 @@ --- title: 'Quickstart' -description: 'From nothing to a resumed, verified run — five steps, no model credential needed until step 4.' +description: 'From zero to a verified, resumable run: scaffold a flow, run it for real, and pick it back up after a crash.' --- -Follow these in order. Steps 1–3 touch nothing that costs money; step 4 makes a real model call. +This walks you from a fresh install to a real run in a few commands: scaffold a flow, run it, and resume it after killing the process mid-way. Everything here runs on your machine against your own agent CLI. ## 1. Install @@ -11,12 +11,6 @@ Follow these in order. Steps 1–3 touch nothing that costs money; step 4 makes npm install -g relayflows ``` - - Not published yet — `relayflows` and `create-flow` are pre-release. Until - they land on npm, clone the SDK and build it locally to run the commands - below against your own checkout. - - ## 2. Scaffold a flow ```bash @@ -25,17 +19,9 @@ cd my-flow npm install ``` -This writes the flow file, a `flows.json` project config, and an npm project with its dependencies already declared. Pass `--cli codex` to target Codex instead of Claude, or `--template deterministic` for a starter that makes no model call at all — useful if you don't have an agent CLI authenticated yet. - -## 3. Check it before running it - -```bash -npx flows check my-flow.flow.ts -``` - -Validates the spec — schema, step graph, verification blocks — without spawning a shell command or a model. This is the same check a CI gate would run. +This writes the flow file, a `flows.json` project config, and an npm project with its dependencies already declared. Pass `--cli codex` to target Codex instead of Claude, or `--template deterministic` for a starter that makes no model call at all. -## 4. Run it +## 3. Run it ```bash npm start @@ -47,9 +33,23 @@ Which runs the generated command directly: flows run my-flow.flow.ts --local-agent --input '{}' ``` -`--local-agent` attaches the existing SDK agent worker to your local daemon and runs your authenticated CLI with its own access — no separate credential to configure. Requires Node 22.18+ and a signed-in `claude` or `codex` on `PATH`. +`--local-agent` attaches the SDK's agent worker to your local daemon and runs your already-authenticated `claude` or `codex` with its own access — nothing extra to configure. Here's a real, captured run of the starter, not a mockup: -## 5. Kill it and resume it +```text +○ run-1 (deterministic) 0.00s +✓ run-1 (deterministic) 1.02s completionReason: success +Hello from Relayflows +○ agent-2 (agent) [agent: preparing] 0.00s +↻ agent-2 (agent) [agent: running] 12.51s +✓ agent-2 (agent) [agent: completed] 28.95s completionReason: success +Hello! How can I help you today? + +RUN 01M20JYM4T9FNGNFX4NQCK34JK completed (3 steps) completionReason: success +``` + +Want to check a flow before spending a model call on it? `npx flows check my-flow.flow.ts` validates the spec — schema, step graph, verification — with nothing spawned. + +## 4. Kill it and resume it Interrupt the process mid-run (`Ctrl-C`, or `kill -9` the daemon) and pick it back up: @@ -57,14 +57,17 @@ Interrupt the process mid-run (`Ctrl-C`, or `kill -9` the daemon) and pick it ba npx flows resume ``` -Steps the journal already recorded as complete don't run again — you'll see the run pick up exactly where it stopped, not from the top. +The step the journal already recorded as complete doesn't run again — the resumed run picks up exactly where it stopped. ## Next - + Write your own steps, verification, and permissions. + + Several named agents, different CLIs, one flow. + Every command: check, run, resume, tick, observe. diff --git a/web/content/docs/relayflows/reliability.mdx b/web/content/docs/relayflows/reliability.mdx new file mode 100644 index 00000000..193d1690 --- /dev/null +++ b/web/content/docs/relayflows/reliability.mdx @@ -0,0 +1,65 @@ +--- +title: 'Reliability' +description: 'The crash-resume contract, the closed failure vocabulary, and the rules that make a flow finishing mean something.' +--- + +An agent's failure mode is wrong output, not no output — a retry built for "it crashed" doesn't catch "it returned something plausible and wrong." Every mechanism on this page exists to close that gap: a flow's completion is something the kernel can prove, not something an agent has to be trusted to report. + +## A run's exit code is a contract + +```text +0 completionReason: success +1 a declared completionReason failure, or an unknown outcome from a transport/runtime/protocol error +2 refused before any journal write: invalid input, failed preflight, unreachable daemon +3 parked — waiting on an llm/agent worker or a needs_human recovery wait +``` + +Exit `2` is the important one to build on: a bad spec, a missing CLI, or an unauthenticated agent fails **before a journal entry is ever written** — at `flows check` time, or at the top of `flows run`, never at minute 27 of a real run. Preflight proves what it can prove up front so a run either starts clean or doesn't start. + +## The closed completion vocabulary + +Every step, and every run, ends in one reason from a fixed set — never a raw stack trace standing in for one: + +```text +Step: success · verification_failed · retries_exhausted · lease_expired · + crashed · timeout · worker_error · budget_exceeded · canceled + +Run: success · step_failed · canceled · budget_exceeded +``` + +A caller with an exhaustive switch over these values never hits a case that isn't there. A step that fails its verification is `verification_failed`, journaled with which check failed — it doesn't get to report success because an agent said it was fine. + +## Kill it mid-step. Resume it. Nothing doubles. + +The crash contract for an agent step is specific: on crash or lease expiry, the workspace is left **dirty, not undone** — the kernel never rolls back an edit that was already made. What happens on the next attempt is a declared recovery mode: + +- **`reset`** (default) — the next attempt restores the pinned workspace revision and starts clean. +- **`inspect`** — the next attempt starts inside the dirty workspace, with the failed attempt's trajectory tail injected as context, and decides whether to continue or redo. +- **`manual`** — the run parks as `needs_human` with a diff of the pinned revision against whatever's actually there. + +Effects are deduped independently of any of that: a writeback — a Slack post, a PR, a file write — is journaled as a fact keyed by `(step id, idempotency key, surface path)`. Two attempts producing the same writeback still produce one provider call. **Exactly-once means exactly-once effects, not exactly-once execution** — an attempt can run more than once; what it's not allowed to do is have that show up twice on the other end. + +## A flow body can't lie about finishing + +Three rules govern an authored TypeScript body, enforced by the runtime rather than left to convention: + +1. **Await every step you create.** `f.run(...)` created and never awaited is `unawaited_step` — a step that might still be running when the flow reports done isn't something the journal can vouch for. +2. **A step's failure is yours whether or not you catch it.** A root failure is recorded before your code can see it; a `catch` can't quietly swallow one. +3. **Finish your derived work before `done()`.** If something chained onto a step is still in flight when the body returns, the run is refused as `unsettled_derived_work` rather than recorded as a success nobody can actually verify. + +Together these mean a run that reports `completionReason: success` really did finish everything it started — not "everything that happened to settle before the process moved on." + +## The daemon doesn't leave you guessing either + +`flows run` and `flows resume` attach to the daemon already serving your data directory, or start one. Concurrent invocations are safe — the daemon holds an exclusive lock, so a redundant one exits without touching anything and its CLI attaches to the winner instead. `--no-spawn` (or `FLOWS_NO_SPAWN=1` for a whole CI environment) asserts a daemon is already present rather than conjuring one — the lever for asserting infrastructure is up, not silently patching around its absence. + +## Next + + + + check, run, resume — every exit code in practice. + + + Verification and permissions, from the author's side. + + diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts index c70f5d63..63a087a8 100644 --- a/web/lib/product-docs-nav.ts +++ b/web/lib/product-docs-nav.ts @@ -232,6 +232,15 @@ export const relayflowsSection: ProductDocSection = { title: 'Run', items: [{ title: 'CLI', slug: 'cli' }], }, + { + title: 'Going further', + items: [ + { title: 'Multi-agent flows', slug: 'multi-agent' }, + { title: 'Cloud', slug: 'cloud' }, + { title: 'Memory & integrations', slug: 'memory-and-integrations' }, + { title: 'Reliability', slug: 'reliability' }, + ], + }, ], }; From 4fb4522a299d65317973e22a8782f15de58d8b7f Mon Sep 17 00:00:00 2001 From: Khaliq Date: Thu, 10 Sep 2026 23:12:39 +0200 Subject: [PATCH 03/15] docs(relayflows): fix CodeRabbit findings; add TypeScript-default/YAML-switch toggle MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Addresses PR #66 review feedback and adds the TS/YAML language switcher: CodeRabbit findings fixed: - quickstart.mdx no longer depends on the unpublished create-flow scaffolder. Verified relayflows@2.0.9 and @relayflows/surface@2.0.9 are real, published packages (create-flow is still 404); rebuilt the quickstart around `npm install @relayflows/surface relayflows` plus a hand-written flow file, with every command and its output re-captured from a real run (both the TypeScript and YAML paths). - cli.mdx: fixed the `flows check` synopsis mismatch — check takes flow.yaml/spec.json only, never a .flow.ts (confirmed empirically: `flows check hello.flow.ts` refuses as invalid_spec). Added a line clarifying a TypeScript flow gets the same preflight inline via `run`. New: TypeScript-by-default, YAML-switchable code samples - Extended the site's existing TypeScript/Python DocsLanguageContext to a third value, 'yaml', reusing the same CodeGroup toggle + sidebar language shows