Repository navigation
docs(blog): add "Promptgramming: Stop Babysitting Your Agents" #68
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
Merged
Merged
Changes from all commits
Commits
Show all changes
12 commits
Select commit
Hold shift + click to select a range
191adfe
docs(blog): add "Promptgramming: Stop Babysitting Your Agents"
khaliqgant 9560f2a
feat(web): add /flows landing page
khaliqgant 3055019
fix(blog): correct Relayflows API usage in promptgramming examples
khaliqgant 90a5767
fix(blog): restore original wording in promptgramming post
khaliqgant 825237b
fix(blog): address valid PR review findings on promptgramming examples
khaliqgant 08372cd
feat(web): flows landing page with live examples, investor strip, /kh…
willwashburn cac9354
Lighten blog post background
willwashburn 1d7b4e6
tweaks and edits
khaliqgant 91bc2c1
Fix mobile flow script scrolling and simplify graph preview
willwashburn a3386c2
Update Flows social image with script in existing brand layout
willwashburn 1a6adcf
Fix integration marquee coverage and About social image
willwashburn 1756715
Update footer product links to Flows and Skip
willwashburn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,13 @@ | ||
| import type { ReactNode } from 'react'; | ||
| import { highlightCode } from '../../lib/syntax'; | ||
| import { flowExamples } from './flow-examples'; | ||
| import { FlowScriptCarousel } from './FlowScriptCarousel'; | ||
|
|
||
| export async function FlowExamples({ introductoryCode }: { introductoryCode: ReactNode }) { | ||
| const examples = await Promise.all(flowExamples.map(async ({ code, ...example }, index) => ({ | ||
| ...example, | ||
| codeHtml: index === 0 ? '' : (await highlightCode(code, 'typescript')).codeHtml, | ||
| }))); | ||
|
|
||
| return <FlowScriptCarousel examples={examples} introductoryCode={introductoryCode} />; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,86 @@ | ||
| 'use client'; | ||
|
|
||
| import { useId, useState, type ReactNode } from 'react'; | ||
| import Link from 'next/link'; | ||
| import { ArrowRight } from 'lucide-react'; | ||
| import s from './flows.module.css'; | ||
|
|
||
| type Example = { | ||
| id: string; | ||
| label: string; | ||
| title: string; | ||
| description: string; | ||
| filename: string; | ||
| codeHtml: string; | ||
| }; | ||
|
|
||
| export function FlowScriptCarousel({ examples, introductoryCode }: { | ||
| examples: Example[]; | ||
| introductoryCode: ReactNode; | ||
| }) { | ||
| const [active, setActive] = useState(0); | ||
| const id = useId(); | ||
| const example = examples[active]; | ||
|
|
||
| return ( | ||
| <div className={`${s.codeSection} ${s.scriptCarousel}`} role="region" aria-roledescription="carousel" aria-label="Example Flow scripts"> | ||
| <div className={s.codeCopy}> | ||
| <div id={`${id}-explanation`} className={s.selectedExplanation}> | ||
| <h2>{active === 0 ? 'A Flow is a script that runs your agents.' : example.title}</h2> | ||
| <p> | ||
| {active === 0 | ||
| ? 'You write the steps in TypeScript: which agent does the work, what it should produce, and what to check before continuing.' | ||
| : example.description} | ||
| </p> | ||
| <p> | ||
| When the flow runs, you've got a dependable, auditable script that babysits your agents for you. | ||
| </p> | ||
| </div> | ||
|
|
||
| <Link href="/docs/relayflows" className={s.textLink}> | ||
| Explore the docs <ArrowRight size={16} aria-hidden="true" /> | ||
| </Link> | ||
| </div> | ||
|
|
||
| <div id={`${id}-script`} className={s.codeBlock} role="group" aria-label={`${example.label} script`}> | ||
| <div className={s.codeHeader}> | ||
| <div className={s.codeChrome} aria-hidden="true"><span /><span /><span /></div> | ||
| <span>{example.filename}</span> | ||
| </div> | ||
| <div className={s.scriptViewport}> | ||
| <div className={s.scriptSizeReference} aria-hidden="true">{introductoryCode}</div> | ||
| <div key={example.id} className={s.scriptScroll} tabIndex={0} role="region" aria-label={`${example.label} TypeScript example`}> | ||
|
|
||
| {active === 0 ? introductoryCode : ( | ||
| <pre | ||
| className={`${s.codePre} ${s.carouselCode}`} | ||
| aria-label={`${example.label} TypeScript example`} | ||
| > | ||
| <code dangerouslySetInnerHTML={{ __html: example.codeHtml }} /> | ||
| </pre> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| <div className={s.useCasePicker}> | ||
| <p id={`${id}-picker-label`} className={s.useCaseLabel}>Explore use cases</p> | ||
| <span className={s.visuallyHidden} aria-live="polite" aria-atomic="true">{example.label}</span> | ||
| <div className={s.useCaseGrid} role="group" aria-labelledby={`${id}-picker-label`}> | ||
| {examples.map((item, index) => ( | ||
| <button | ||
| key={item.id} | ||
| type="button" | ||
| className={s.useCaseOption} | ||
| aria-pressed={active === index} | ||
| aria-controls={`${id}-explanation ${id}-script`} | ||
| onClick={() => setActive(index)} | ||
| > | ||
| <span className={s.useCaseNumber}>{String(index + 1).padStart(2, '0')}</span> | ||
| {item.label} | ||
| </button> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| 'use client'; | ||
|
|
||
| import { useLayoutEffect, useRef, useState, type CSSProperties } from 'react'; | ||
| import Image from 'next/image'; | ||
| import { DatabaseBackup } from 'lucide-react'; | ||
| import { SiPostgresql, SiRedis, SiPosthog } from 'react-icons/si'; | ||
| import s from './flows.module.css'; | ||
|
|
||
| function brand(name: string, slug: string) { | ||
| return { name, logo: <Image src={`/integration-logos/${slug}.svg`} alt="" width={36} height={36} /> }; | ||
| } | ||
|
|
||
| const integrations = [ | ||
| brand('GitHub', 'github'), | ||
| brand('Linear', 'linear'), | ||
| brand('Slack', 'slack'), | ||
| brand('Jira', 'jira'), | ||
| brand('Notion', 'notion'), | ||
| brand('Stripe', 'stripe'), | ||
| brand('HubSpot', 'hubspot'), | ||
| brand('Intercom', 'intercom'), | ||
| { name: 'Postgres', logo: <SiPostgresql size={36} aria-hidden="true" /> }, | ||
| { name: 'Redis', logo: <SiRedis size={36} aria-hidden="true" /> }, | ||
| { name: 'S3', logo: <DatabaseBackup size={36} aria-hidden="true" /> }, | ||
| brand('Cloudflare', 'cloudflare'), | ||
| brand('Sendgrid', 'sendgrid'), | ||
| brand('Mailgun', 'mailgun'), | ||
| brand('Gmail', 'gmail'), | ||
| brand('Google Calendar', 'google-calendar'), | ||
| brand('Segment', 'segment'), | ||
| { name: 'PostHog', logo: <SiPosthog size={36} aria-hidden="true" /> }, | ||
| ]; | ||
|
|
||
| function IntegrationRow({ row }: { row: typeof integrations }) { | ||
| const trackRef = useRef<HTMLDivElement>(null); | ||
| const [copies, setCopies] = useState(2); | ||
|
|
||
| useLayoutEffect(() => { | ||
| const track = trackRef.current; | ||
| const window = track?.parentElement; | ||
| const group = track?.firstElementChild; | ||
| if (!track || !window || !group) return; | ||
|
|
||
| const resize = () => { | ||
| const groupWidth = group.getBoundingClientRect().width; | ||
| if (groupWidth > 0) { | ||
| setCopies(Math.max(2, Math.ceil(window.clientWidth / groupWidth) + 1)); | ||
| } | ||
| }; | ||
| resize(); | ||
| const observer = new ResizeObserver(resize); | ||
| observer.observe(window); | ||
| observer.observe(group); | ||
| return () => observer.disconnect(); | ||
| }, []); | ||
|
|
||
| return ( | ||
| <div ref={trackRef} className={s.integrationTrack} | ||
| style={{ '--integration-copies': copies } as CSSProperties}> | ||
| {Array.from({ length: copies }, (_, copy) => ( | ||
| <ul key={copy} className={s.integrationGroup} aria-hidden={copy > 0 || undefined}> | ||
| {row.map(({ name, logo }) => ( | ||
| <li key={name} className={s.integrationLogo}> | ||
| {logo} | ||
| <span>{name}</span> | ||
| </li> | ||
| ))} | ||
| </ul> | ||
| ))} | ||
| </div> | ||
| ); | ||
| } | ||
|
|
||
| export function IntegrationMarquee() { | ||
| return ( | ||
| <div className={s.integrationsMarquee}> | ||
| <div className={s.integrationWindow}> | ||
| {[integrations.slice(0, 9), integrations.slice(9)].map((row, index) => ( | ||
| <IntegrationRow key={index} row={row} /> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| export const flowExamples = [ | ||
| { | ||
| "id": "software-factory", | ||
| "label": "Software factory", | ||
| "title": "Turn a ticket into a pull request.", | ||
| "description": "Plan the change, write the code, and run the tests before opening a PR.", | ||
| "filename": "software-factory.flow.ts", | ||
| "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repo: string;\n ticket: string;\n};\n\nexport default flow<Input>(\n \"software-factory\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"planner\", {\n task: `${input.ticket} Write a plan to plan.md.`,\n }).gate((r) => r.artifacts.includes(\"plan.md\"));\n\n await f.agent(\"implementer\", {\n task: \"Read plan.md. Implement it on branch flow/fix. \" +\n \"Write the PR description to summary.md.\",\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n // The tests run outside the agent.\n // The agent cannot lie about the exit code.\n await f.run(\"git checkout flow/fix && npm test\");\n\n await f.agent(\"reviewer\", {\n task: \"Review the diff against main. \" +\n \"Write review.passed only if ready for a PR.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Deterministic step, not an agent decision.\n await f.github.createPullRequest({\n repo: input.repo,\n head: \"flow/fix\",\n base: \"main\",\n bodyPath: \"summary.md\",\n });\n f.done(\"success\");\n },\n);" | ||
| }, | ||
| { | ||
| "id": "pr-review", | ||
| "label": "Code review", | ||
| "title": "Give every PR a second opinion.", | ||
| "description": "Review a diff for security, correctness, and performance in parallel. Then reconcile the findings.", | ||
| "filename": "pr-review.flow.ts", | ||
| "code": "import { flow } from \"@relayflows/surface\";\n\nexport default flow(\n \"pr-review\",\n { budget: \"$5/run\" },\n async (f) => {\n const diff = await f.run(\"git diff main...HEAD\");\n const lenses = [\"security\", \"correctness\", \"performance\"];\n\n await Promise.all(lenses.map((lens) =>\n f.agent(`${lens}-reviewer`, {\n task: `Review this diff for ${lens}: ${diff}. ` +\n `Write findings to review/${lens}.md.`,\n workspace: \"review/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(`review/${lens}.md`)\n )\n ));\n\n // Resolve disagreements between reviewers.\n await f.agent(\"reconciler\", {\n task: \"Read the three reviews in review/. \" +\n \"Resolve disagreements, flag unresolved issues, \" +\n \"and write review/consensus.md.\",\n workspace: \"review/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(\"review/consensus.md\")\n );\n f.done(\"success\");\n },\n);" | ||
| }, | ||
| { | ||
| "id": "support-triage", | ||
| "label": "Customer support", | ||
| "title": "Draft the reply. Let a human decide.", | ||
| "description": "Draft a response to a support thread, get approval, and reply in Slack.", | ||
| "filename": "support-triage.flow.ts", | ||
| "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n message: string;\n channel: string;\n threadTs: string;\n approver: string;\n};\n\nexport default flow<Input>(\n \"support-triage\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Classify this request: ${input.message}. ` +\n \"Write the category and urgency to triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"writer\", {\n task: `Read triage.md and draft a reply to: ` +\n `${input.message}. Write only the reply to reply.md.`,\n }).gate((r) => r.artifacts.includes(\"reply.md\"));\n\n const reply = await f.run(\"cat reply.md\");\n const approved = await f.human(\n `Send this reply?\\n\\n${reply}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n // Send only after a human approves.\n await f.slack.reply(input.channel, input.threadTs, reply);\n f.done(\"success\");\n },\n);" | ||
| }, | ||
| { | ||
| "id": "content-pipeline", | ||
| "label": "Content publishing", | ||
| "title": "Put a fact-check between draft and publish.", | ||
| "description": "Research, draft, and check a post before a human approves sharing it in Slack.", | ||
| "filename": "content-pipeline.flow.ts", | ||
| "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n topic: string;\n channel: string;\n approver: string;\n};\n\nexport default flow<Input>(\n \"content-pipeline\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"researcher\", {\n task: `Research ${input.topic}. Cite your sources. ` +\n \"Write research.md.\",\n }).gate((r) => r.artifacts.includes(\"research.md\"));\n\n await f.agent(\"writer\", {\n task: \"Use research.md to write post.md. \" +\n \"Keep the source links with each claim.\",\n }).gate((r) => r.artifacts.includes(\"post.md\"));\n\n await f.agent(\"fact-checker\", {\n task: \"Check post.md against its sources. \" +\n \"Write checked.passed only if all claims hold up.\",\n }).gate((r) => r.artifacts.includes(\"checked.passed\"));\n\n const post = await f.run(\"cat post.md\");\n const approved = await f.human(\n `Publish this post?\\n\\n${post}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n await f.slack.post(input.channel, post);\n f.done(\"success\");\n },\n);" | ||
| }, | ||
| { | ||
| "id": "repo-migration", | ||
| "label": "Repo migrations", | ||
| "title": "Make the same change across repositories.", | ||
| "description": "Apply a migration in each local checkout, test it, and open a PR.", | ||
| "filename": "repo-migration.flow.ts", | ||
| "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n repos: { path: string; slug: string }[];\n instructions: string;\n};\n\nexport default flow<Input>(\n \"repo-migration\",\n { budget: \"$5/run\" },\n async (f, input) => {\n for (const repo of input.repos) {\n // Quote the local checkout path for shell commands.\n const dir = \"'\" + repo.path.replaceAll(\"'\", \"'\\\"'\\\"'\") + \"'\";\n\n await f.agent(`migrate-${repo.slug}`, {\n cwd: repo.path,\n task: `${input.instructions} ` +\n \"Use branch flow/migration and commit the change. \" +\n \"Write migration.md with a summary.\",\n }).gate((r) => r.artifacts.includes(\"migration.md\"));\n\n await f.run(`cd ${dir} && npm test`);\n\n await f.agent(`review-${repo.slug}`, {\n cwd: repo.path,\n task: \"Review this migration against main. \" +\n \"Write review.passed only if it is ready.\",\n }).gate((r) => r.artifacts.includes(\"review.passed\"));\n\n // Each checkout opens its own pull request.\n await f.run(\n `cd ${dir} && gh pr create --base main ` +\n '--head flow/migration --title \"Apply migration\" ' +\n '--body-file migration.md'\n );\n }\n f.done(\"success\");\n },\n);" | ||
| }, | ||
| { | ||
| "id": "voicemail-follow-up", | ||
| "label": "Voicemail follow-up", | ||
| "title": "Turn a voicemail into a callback brief.", | ||
| "description": "Use a transcript to prepare the callback, then send the approved brief to a teammate.", | ||
| "filename": "voicemail-follow-up.flow.ts", | ||
| "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n transcript: string;\n approver: string;\n callbackOwner: string;\n};\n\nexport default flow<Input>(\n \"voicemail-follow-up\",\n { budget: \"$5/run\" },\n async (f, input) => {\n await f.agent(\"triage\", {\n task: `Read this voicemail: ${input.transcript}. ` +\n \"Identify urgency and the caller's request. \" +\n \"Write triage.md.\",\n }).gate((r) => r.artifacts.includes(\"triage.md\"));\n\n await f.agent(\"callback-writer\", {\n task: \"Read triage.md. Prepare a callback brief \" +\n \"with the questions we need to answer. \" +\n \"Write callback.md.\",\n }).gate((r) => r.artifacts.includes(\"callback.md\"));\n\n const brief = await f.run(\"cat callback.md\");\n const approved = await f.human(\n `Ready for a callback?\\n\\n${brief}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n await f.slack.dm(input.callbackOwner, brief);\n f.done(\"success\");\n },\n);" | ||
| }, | ||
| { | ||
| "id": "research-report", | ||
| "label": "Research reports", | ||
| "title": "Bring independent research into one report.", | ||
| "description": "Give researchers different angles, combine their findings, and ask another agent to check the citations.", | ||
| "filename": "research-report.flow.ts", | ||
| "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n question: string;\n};\n\nexport default flow<Input>(\n \"research-report\",\n { budget: \"$5/run\" },\n async (f, input) => {\n const angles = [\"technical\", \"commercial\", \"risks\"];\n\n await Promise.all(angles.map((angle) =>\n f.agent(`research-${angle}`, {\n task: `Research ${input.question} from a ${angle} ` +\n `angle. Cite sources in research/${angle}.md.`,\n workspace: \"research/: readwrite\",\n }).gate((r) =>\n r.artifacts.includes(`research/${angle}.md`)\n )\n ));\n\n await f.agent(\"editor\", {\n task: \"Read research/*.md. Combine the findings, \" +\n \"keep disagreements visible, and cite sources. \" +\n \"Write report.md.\",\n }).gate((r) => r.artifacts.includes(\"report.md\"));\n\n await f.agent(\"citation-reviewer\", {\n task: \"Open every source in report.md and check \" +\n \"the claims it supports. Write citations.passed \" +\n \"only when every citation checks out.\",\n }).gate((r) => r.artifacts.includes(\"citations.passed\"));\n f.done(\"success\");\n },\n);" | ||
| }, | ||
| { | ||
| "id": "redacted-summary", | ||
| "label": "Sensitive data", | ||
| "title": "Choose what the summarizer gets to see.", | ||
| "description": "Select the fields needed for the task before putting a record into the agent prompt.", | ||
| "filename": "redacted-summary.flow.ts", | ||
| "code": "import { flow } from \"@relayflows/surface\";\n\ntype Input = {\n record: { category: string; status: string; email: string };\n approver: string;\n};\n\nexport default flow<Input>(\n \"redacted-summary\",\n { budget: \"$5/run\" },\n async (f, input) => {\n // Select fields in code before building the prompt.\n // The email address is excluded from this input.\n const selected = {\n category: input.record.category,\n status: input.record.status,\n };\n\n await f.agent(\"summarizer\", {\n task: `Summarize this record: ` +\n `${JSON.stringify(selected)}. Write summary.md.`,\n }).gate((r) => r.artifacts.includes(\"summary.md\"));\n\n const summary = await f.run(\"cat summary.md\");\n const approved = await f.human(\n `Review this summary before sharing:\\n\\n${summary}`,\n { to: input.approver },\n );\n if (!approved) return f.done(\"canceled\");\n\n f.done(\"success\");\n },\n);" | ||
| } | ||
| ]; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Make the Software factory example selectable.
The page intentionally supplies introductory code for
active === 0, and that code includes a Software factory flow. However,examples[0]still contains a separate title, description, filename, and full code sample. The first button selects index 0, so thoseexamples[0]fields never render. The page remains usable, so this is a minor functional issue.Represent the introduction as a separate slide, then map the “Software factory” button to
examples[0].🤖 Prompt for AI Agents