Repository navigation
Finish Relayflows product docs #66
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
11cac5c
fd903fe
4fb4522
10e8154
2352509
7074816
bc02ddf
33cda75
8b1b8ca
9a3053b
f1dfce9
af7ec54
e3c554d
24eab76
70f3fc7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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( | ||
| <DefaultVariant | ||
| headingFamily={headingFamily} | ||
| bodyFamily={bodyFamily} | ||
| eyebrow={relayflowsSection.label} | ||
| title={doc.frontmatter.title} | ||
| subtitle={doc.frontmatter.description} | ||
| />, | ||
| { | ||
| ...OG_SIZE, | ||
| ...(fonts.length > 0 ? { fonts } : {}), | ||
| } | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<Metadata> { | ||
| 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 <ProductDocPage sectionId={SECTION_ID} slug={slug} />; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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', | ||
| }, | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| import { redirect } from 'next/navigation'; | ||
|
|
||
| export default function RelayflowsDocsIndexPage() { | ||
| redirect('/docs/relayflows/introduction'); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -14,6 +14,21 @@ export function TableOfContents({ items }: { items: TocItem[] }) { | |
| const { language, setLanguage } = useDocsLanguage(); | ||
| const pathname = usePathname() ?? '/docs'; | ||
| const productSection = getProductSectionForPath(pathname); | ||
| const secondaryLanguage = productSection?.id === 'relayflows' ? 'yaml' : 'python'; | ||
| const secondaryLanguageLabel = secondaryLanguage === 'yaml' ? 'YAML' : 'Python'; | ||
|
|
||
| // The select can only show 'typescript' or this section's secondaryLanguage. | ||
| // A stored preference from a different section (e.g. 'python' while viewing | ||
| // a relayflows page) falls back to displaying 'typescript' without this | ||
| // effect, but the underlying language state stays unchanged — so the | ||
| // dropdown and the actual CodeGroup selection drift apart, and reselecting | ||
| // the already-displayed option produces no onChange to correct it. Write | ||
| // the fallback back to real state so display and state always agree. | ||
| useEffect(() => { | ||
| if (language !== 'typescript' && language !== secondaryLanguage) { | ||
| setLanguage('typescript'); | ||
| } | ||
| }, [language, secondaryLanguage, setLanguage]); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Language preference wiped across sectionsMedium Severity The new effect writes Reviewed by Cursor Bugbot for commit 33cda75. Configure here. |
||
|
|
||
| useEffect(() => { | ||
| const headings = items.map((item) => document.getElementById(item.id)).filter(Boolean) as HTMLElement[]; | ||
|
|
@@ -43,11 +58,11 @@ export function TableOfContents({ items }: { items: TocItem[] }) { | |
| <select | ||
| className={styles.tocSelect} | ||
| aria-label="Select docs language" | ||
| value={language} | ||
| onChange={(event) => setLanguage(event.target.value as 'typescript' | 'python')} | ||
| value={language === secondaryLanguage ? secondaryLanguage : 'typescript'} | ||
| onChange={(event) => setLanguage(event.target.value as 'typescript' | 'python' | 'yaml')} | ||
|
cursor[bot] marked this conversation as resolved.
|
||
| > | ||
| <option value="typescript">TypeScript</option> | ||
| <option value="python">Python</option> | ||
| <option value={secondaryLanguage}>{secondaryLanguageLabel}</option> | ||
| </select> | ||
| </label> | ||
| </div> | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,144 @@ | ||
| --- | ||
| 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 did its job. If you can't state that proof yet, figure it out first, before wiring the step up. | ||
|
|
||
| ## Use the skill | ||
|
|
||
| Don't paste this page into your agent's context — install the [writing-relayflows](https://github.com/AgentWorkforce/skills/blob/main/skills/writing-relayflows/SKILL.md) skill and let it author flows directly: | ||
|
|
||
| ```bash | ||
| # with prpm | ||
| npx prpm install @agent-relay/writing-relayflows | ||
|
|
||
| # with skills.sh | ||
| npx skills add https://github.com/agentworkforce/skills --skill writing-relayflows | ||
| ``` | ||
|
|
||
| The rest of this page is what the skill encodes — worth reading so you can tell whether what it wrote is right. | ||
|
|
||
| ## Two ways to author the same thing | ||
|
|
||
| **YAML** describes a fixed set of steps and their dependencies as data, so `flows check` or a CI gate can read and validate it without running anything. **TypeScript** calls the same primitives imperatively, as ordinary code. Both compile down to the same journal. | ||
|
|
||
| <CodeGroup> | ||
| ```typescript TypeScript | ||
| import { flow } from '@relayflows/surface'; | ||
|
|
||
| export default flow('hello-agent', async (f) => { | ||
| const greeting = await f.run('printf hello'); | ||
| const edit = await f.agent('edit', { | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| task: 'Produce the hello artifact.', | ||
| cli: 'claude', | ||
| model: 'claude-sonnet-4-6', | ||
| }); | ||
| const finish = await f.run('printf done'); | ||
| f.done('success'); | ||
| }); | ||
| ``` | ||
| ```yaml 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.' | ||
| cli: claude | ||
| model: claude-sonnet-4-6 | ||
| verification: | ||
| type: output_contains | ||
| value: agent-ok | ||
|
|
||
| - id: finish | ||
| type: deterministic | ||
| dependsOn: [edit] | ||
| command: 'printf done' | ||
| ``` | ||
| </CodeGroup> | ||
|
|
||
| Reach for YAML when you want the whole flow readable at a glance and checkable in CI. Reach for TypeScript when a step's next move depends on what a previous one returned: an `f.human` approval, ordinary `if`/`for` logic, an `f.dispatch` to a child flow. | ||
|
|
||
| Both `edit` steps above name their own `cli` and `model` directly, the same way in either language ([flows#310](https://github.com/AgentWorkforce/flows/issues/310)). Neither is required in TypeScript: omit them and the step falls back to the flow's `cli`, then the nearest `flows.json`'s project-wide default, the same resolution [Introduction](/docs/relayflows/introduction#a-whole-flow) covers. | ||
|
|
||
| <Note> | ||
| `recoveryMode`, `permissions`, `surfaces`, and `budget`, the richer | ||
| fields covered further down, are YAML/JSON fields only today. | ||
| TypeScript's `Ctx.agent` doesn't have them. If a step needs them, | ||
| author it in YAML and either run it directly or have a TypeScript | ||
| flow reach it with `f.dispatch`. | ||
| </Note> | ||
|
|
||
| ## The context a flow body gets | ||
|
|
||
| ```ts | ||
| interface Ctx { | ||
| run(command: string): Step<string>; | ||
| llm(strings: TemplateStringsArray, ...values: unknown[]): Step<string>; | ||
| agent(name: string, options: { task: string; workspace?: string; cli?: string; model?: string }): Step<{ summary: string; artifacts: string[] }>; | ||
| human(question: string, options: { to: string }): Promise<boolean>; | ||
| dispatch<T>(flow: string, input: unknown): Promise<T>; | ||
| done(reason: 'success' | 'step_failed' | 'canceled' | 'budget_exceeded'): void; | ||
| } | ||
| ``` | ||
|
|
||
| This is 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 instead of a blocking call — 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 | ||
|
|
||
| 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 is recorded as `verification_failed`, with exactly which check failed. The agent insisting it went fine doesn't override that. | ||
|
|
||
| ## Permissions and recovery | ||
|
|
||
| An `agent` step declares what it's allowed to touch — `fileGlobs`, `accessPreset` — and what happens if it crashes mid-edit: | ||
|
|
||
| ```yaml | ||
| - id: edit | ||
| type: agent | ||
| dependsOn: [greet] | ||
| instruction: 'Produce the hello artifact through the deterministic test stub.' | ||
| recoveryMode: reset | ||
| maxIterations: 2 | ||
| surfaces: | ||
| workspace: | ||
| - surface: repo | ||
| streams: | ||
| - stream: agent-notes | ||
| permissions: | ||
| fileGlobs: ['artifacts/**'] | ||
| accessPreset: readwrite | ||
| verification: | ||
| type: output_contains | ||
| value: agent-ok | ||
| ``` | ||
|
|
||
| - **`reset`** (the default) — the next attempt starts fresh from the pinned workspace revision, with no half-finished edit left behind. | ||
| - **`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` caps how many attempts a step gets before one of those three outcomes has to happen. A run-level `budget` (`maxTokensIn`, `maxTokensOut`, `maxDollars`) caps the whole flow the same way, declared once and enforced by the kernel instead of tracked by hand. | ||
|
|
||
| ## Next | ||
|
|
||
| <CardGroup cols={2}> | ||
| <Card title="Quickstart" href="/docs/relayflows/quickstart"> | ||
| Scaffold one and run it end to end. | ||
| </Card> | ||
| <Card title="CLI" href="/docs/relayflows/cli"> | ||
| check, run, resume — every flag. | ||
| </Card> | ||
| </CardGroup> | ||


Uh oh!
There was an error while loading. Please reload this page.