+
+
+
+ {agent.name}
+ {agent.tagline}
+ {/* Its own reserved line, so revealing it on hover/focus
+ never shifts the row or hides the tagline. */}
+
+
+ {agent.trigger.kind}
- ))}
-
diff --git a/web/app/agents/agents.module.css b/web/app/agents/agents.module.css
index 6261b1c..9ead1b5 100644
--- a/web/app/agents/agents.module.css
+++ b/web/app/agents/agents.module.css
@@ -203,104 +203,244 @@
}
}
-/* ── Gallery grid ── */
+/* ── Gallery list ── */
.gallery {
- max-width: 1280px;
+ max-width: 1180px;
margin: 0 auto;
padding: 24px 40px 96px;
}
-.grid {
+/* Column headers. Mirrors the .rowLink grid; hidden once the row stacks. */
+.listHead {
display: grid;
- grid-template-columns: repeat(3, minmax(0, 1fr));
- gap: 24px;
-}
-
-.col {
- display: flex;
+ grid-template-columns: minmax(0, 1fr) 190px 20px;
+ gap: 20px;
+ align-items: center;
+ padding: 0 18px 10px;
+ font-size: 0.7rem;
+ font-weight: 600;
+ letter-spacing: 0.08em;
+ text-transform: uppercase;
+ color: var(--fg-subtle, var(--fg-muted));
}
-.card {
- display: flex;
- flex-direction: column;
- min-height: 100%;
- width: 100%;
- border-radius: 18px;
+.list {
+ list-style: none;
+ margin: 0;
+ padding: 0;
border: 1px solid var(--line);
+ border-radius: 14px;
background: var(--card-bg);
overflow: hidden;
- text-decoration: none;
- color: inherit;
- box-shadow: 0 8px 24px color-mix(in srgb, var(--primary) 4%, transparent);
- transition:
- border-color 0.2s,
- box-shadow 0.2s,
- transform 0.2s;
}
-.card:hover {
- border-color: var(--card-hover-border);
- box-shadow: 0 14px 36px color-mix(in srgb, var(--primary) 12%, transparent);
- transform: translateY(-3px);
+.row {
+ display: block;
}
-.cardMedia {
- position: relative;
- aspect-ratio: 16 / 9;
- width: 100%;
- overflow: hidden;
- border-bottom: 1px solid var(--line);
+.row + .row {
+ border-top: 1px solid var(--line);
}
-.cardMedia img {
- width: 100%;
- height: 100%;
- object-fit: cover;
- display: block;
+.rowLink {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 190px 20px;
+ gap: 20px;
+ align-items: center;
+ padding: 13px 18px;
+ text-decoration: none;
+ color: inherit;
+ transition: background 0.15s;
+}
+
+.rowLink:hover {
+ background: color-mix(in srgb, var(--primary) 5%, transparent);
}
-.cardBody {
+.rowMain {
display: flex;
flex-direction: column;
- gap: 10px;
- padding: 20px 22px 22px;
- flex: 1;
+ gap: 3px;
+ min-width: 0;
}
-.cardName {
+.rowName {
font-family: var(--font-heading), sans-serif;
- font-size: 1.18rem;
+ font-size: 1rem;
font-weight: 600;
color: var(--fg);
- margin: 0;
}
-.cardTagline {
- font-size: 0.94rem;
- line-height: 1.55;
+.rowTagline {
+ font-size: 0.86rem;
+ line-height: 1.45;
color: var(--fg-muted);
- margin: 0;
- flex: 1;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
}
-.cardFooter {
+/* Occupies a permanently reserved line under the tagline, so fading it in on
+ hover/focus neither shifts the row nor hides the description. */
+.rowTrigger {
display: flex;
align-items: center;
- justify-content: space-between;
- gap: 12px;
- margin-top: 4px;
+ gap: 8px;
+ min-width: 0;
+ font-size: 0.78rem;
+ line-height: 1.45;
+ color: var(--fg-muted);
+ opacity: 0;
+ transition: opacity 0.15s;
+}
+
+.rowLink:hover .rowTrigger,
+.rowLink:focus-visible .rowTrigger {
+ opacity: 1;
}
-.cardArrow {
- font-size: 0.9rem;
+.rowTriggerText {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+/* No hover on touch — keep the trigger visible rather than unreachable. */
+@media (hover: none) {
+ .rowTrigger {
+ opacity: 1;
+ }
+}
+
+.kindSchedule,
+.kindEvent {
+ flex: none;
+ padding: 2px 8px;
+ border-radius: 100px;
+ font-size: 0.68rem;
font-weight: 600;
+ letter-spacing: 0.02em;
+ text-transform: uppercase;
+}
+
+.kindSchedule {
+ border: 1px solid color-mix(in srgb, var(--primary) 22%, transparent);
+ background: color-mix(in srgb, var(--primary) 9%, transparent);
color: var(--primary);
- white-space: nowrap;
+}
+
+.kindEvent {
+ border: 1px solid color-mix(in srgb, var(--fg-muted) 26%, transparent);
+ background: color-mix(in srgb, var(--fg-muted) 9%, transparent);
+ color: var(--fg-muted);
+}
+
+.rowLogos {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px;
+}
+
+/* Provider marks are drawn for light surfaces and several are solid black
+ (github, granola, notion), so they get their own light chip rather than
+ sitting directly on the row — otherwise they vanish in dark mode.
+ Height-constrained with auto width so wordmarks keep their aspect ratio. */
+.rowLogo {
+ height: 16px;
+ width: auto;
+ max-width: 64px;
+ object-fit: contain;
+ display: block;
+ box-sizing: content-box;
+ padding: 3px 5px;
+ border-radius: 5px;
+ background: #fff;
+ border: 1px solid color-mix(in srgb, var(--fg) 10%, transparent);
+}
+
+.rowArrow {
+ justify-self: end;
+ font-size: 0.95rem;
+ color: var(--fg-muted);
+ transition: transform 0.15s, color 0.15s;
+}
+
+.rowLink:hover .rowArrow {
+ color: var(--primary);
+ transform: translateX(2px);
+}
+
+@media (max-width: 900px) {
+ .gallery {
+ padding: 24px 20px 72px;
+ }
+
+ .listHead {
+ display: none;
+ }
+
+ .rowLink {
+ grid-template-columns: minmax(0, 1fr) auto;
+ grid-template-areas:
+ 'main arrow'
+ 'logos arrow';
+ gap: 10px 16px;
+ align-items: start;
+ padding: 16px 18px;
+ }
+
+ .rowMain {
+ grid-area: main;
+ }
+
+ .rowLogos {
+ grid-area: logos;
+ }
+
+ .rowArrow {
+ grid-area: arrow;
+ align-self: center;
+ }
}
/* ── Integration / meta chips ── */
+/* Labeled provider chip used on the detail page — mark plus name, where
+ there's room to spell it out. */
+.logoChip {
+ display: inline-flex;
+ align-items: center;
+ gap: 7px;
+ padding: 4px 11px 4px 5px;
+ border-radius: 100px;
+ border: 1px solid color-mix(in srgb, var(--primary) 16%, transparent);
+ background: color-mix(in srgb, var(--primary) 7%, transparent);
+ font-size: 0.78rem;
+ font-weight: 600;
+ color: var(--primary);
+}
+
+/* Same light chip as the gallery marks — several provider logos are solid
+ black and would disappear on the dark surface. */
+.logoChipIcon {
+ height: 14px;
+ width: auto;
+ max-width: 52px;
+ object-fit: contain;
+ display: block;
+ box-sizing: content-box;
+ padding: 3px 4px;
+ border-radius: 100px;
+ background: #fff;
+}
+
+/* A chip with no logo loses the icon's left inset, so even it back out. */
+.logoChip:not(:has(img)) {
+ padding-left: 11px;
+}
+
.chips {
display: flex;
flex-wrap: wrap;
diff --git a/web/components/agents/IntegrationLogos.tsx b/web/components/agents/IntegrationLogos.tsx
new file mode 100644
index 0000000..57be732
--- /dev/null
+++ b/web/components/agents/IntegrationLogos.tsx
@@ -0,0 +1,61 @@
+import { INTEGRATION_LABELS, integrationLogo, type Integration } from '../../lib/agents';
+
+/**
+ * Provider logos for an agent.
+ *
+ * Bare (gallery rows): just the marks, for density.
+ * `withLabel` (detail page): mark + provider name, where there's room to spell
+ * it out. Providers Nango has no logo for fall back to a text-only chip so the
+ * set still reads correctly either way.
+ *
+ * Plain (not next/image) to match AgentArt — the SST/OpenNext optimizer
+ * 500s without sharp.
+ */
+export function IntegrationLogos({
+ integrations,
+ withLabel = false,
+ className,
+ chipClassName,
+ logoClassName,
+}: {
+ integrations: Integration[];
+ /** Render the provider name next to its mark. */
+ withLabel?: boolean;
+ className?: string;
+ /** Text-only chip: the labeled variant, and the no-logo fallback. */
+ chipClassName?: string;
+ logoClassName?: string;
+}) {
+ return (
+
+ {integrations.map((integration) => {
+ const label = INTEGRATION_LABELS[integration];
+ const src = integrationLogo(integration);
+
+ if (withLabel) {
+ return (
+
+ {src ? : null}
+ {label}
+
+ );
+ }
+
+ return src ? (
+
+ ) : (
+
+ {label}
+
+ );
+ })}
+
+ );
+}
diff --git a/web/content/docs/agents/build.mdx b/web/content/docs/agents/build.mdx
index 0db2a95..9071121 100644
--- a/web/content/docs/agents/build.mdx
+++ b/web/content/docs/agents/build.mdx
@@ -1,47 +1,99 @@
---
title: 'Build your own'
-description: 'Define a custom agent by making the trigger, context, permissions, and handoff path explicit.'
+description: 'Author a cloud persona: persona.json for deploy wiring, agent.ts for behavior.'
---
-Start with the contract. The implementation can change, but the contract keeps the agent understandable.
-
-## Define the job
-
-Write the job in one sentence:
+Start with the job in one sentence:
```text
When happens, inspect , take , and report .
```
-If the sentence needs a paragraph, split the agent into smaller agents.
+If that needs a paragraph, split it into smaller agents.
+
+## Use the skill
+
+Don't hand-write the shape from memory — it moves. Install the skill and let your coding agent do the authoring:
+
+```bash
+# with prpm
+npx prpm install @agent-relay/creating-cloud-persona
+
+# with skills.sh
+npx skills add https://github.com/agentworkforce/skills --skill creating-cloud-persona
+```
+
+Then ask it to "create a cloud persona for <job>". The skill covers the current persona shape, the event model, integration scopes and adapter config, inputs, memory, sandbox modes, and the production traps that break deploys.
+
+The rest of this page is what the skill encodes — worth reading so you can tell whether what it wrote is right.
+
+## The two files
+
+A cloud persona is always a pair:
-## Give it a persona
+- **`persona.json`** — deploy and runtime wiring: the integrations it requires and their scopes, inputs, model and harness, memory, sandbox mode, and the system prompt that defines its role.
+- **`agent.ts`** — the behavior. `defineAgent(...)` declares the triggers, schedules, and watch rules; the handler branches on `event.type` and does the work.
-The persona is not a brand voice. It is the operating policy:
+Wakeups are declarative, behavior is imperative. Every provider your `agent.ts` listens on must also be declared in `persona.json` — an undeclared integration never mounts, and its triggers never fire.
-- What the agent optimizes for
-- What it must never do without approval
-- Which files, tools, providers, and channels it can use
-- What a good final report looks like
+## Complexity stays declarative
+
+Complex agents aren't more code — they're more declarations. The [`review`](https://github.com/AgentWorkforce/agents/tree/main/review) agent reviews PRs, pushes fixes, and merges on approval. Its entire wakeup surface:
+
+```ts
+export default defineAgent({
+ triggers: {
+ github: [
+ { on: 'pull_request.opened' },
+ { on: 'pull_request.synchronize' },
+ { on: 'pull_request_review.submitted' },
+ { on: 'pull_request_review_comment.created' },
+ { on: 'check_run.completed' },
+ { on: 'issue_comment.created' }
+ ],
+ slack: [{ on: 'message.created', paths: ['/slack/channels/${SLACK_CHANNEL}/**'] }]
+ },
+ handler: async (ctx, event) => {
+ const data = (await event.expand('full')).data;
+ if (event.type.startsWith('slack.')) return handleSlackMergeRequest(ctx, data);
+ // ...route the github events
+ }
+});
+```
+
+Seven event sources across two providers, and `event` is fully narrowed to exactly those types — an unhandled case is a type error, not a 3am surprise.
+
+The heavy lifting delegates to a coding harness in one call:
+
+```ts
+const run = await ctx.harness.run({ cwd: ctx.sandbox.cwd, prompt: reviewHarnessPrompt(pr) });
+```
+
+The PR's repo is already materialized at `ctx.sandbox.cwd`; the agent edits files there and cloud commits and pushes after the harness exits. No git, no `gh`, no checkout logic in your code.
## Keep context structured
-Prefer durable references over pasted prose:
+Prefer durable references over pasted prose: Relay channels and threads, Relayfile paths, PR URLs and commit SHAs, Linear issue IDs. Structured context lets the agent resume, verify, and explain itself.
-- Relay channels and threads for coordination
-- Relayfile paths for provider-backed records
-- GitHub PR URLs and commit SHAs for code review
-- Linear issue IDs for product work
+## Verify before deploying
+
+```bash
+npx agentworkforce persona compile ./my-agent/persona.ts # if typed
+npx agentworkforce invoke ./my-agent/persona.json --schedule daily
+npx agentworkforce deploy ./my-agent/persona.json --mode cloud --dry-run
+```
-Structured context lets the agent resume, verify, and explain itself without reconstructing the world from a prompt.
+`invoke` runs the handler locally against a fixture or a named schedule and prints a run record. The dry-run validates the deploy without side effects.
-## Build from the repo
+
+ Every command takes the `persona.json` **file**, not the directory — a directory path fails with `EISDIR`. If you author a typed `persona.ts`, `persona compile` writes the JSON next to it.
+
- Fork the examples and keep your agent's persona, trigger, and implementation together.
+ Fork the examples — persona, trigger, and implementation live together.
-
- Use Relay for the messaging, workspace, and handoff layer.
+
+ Ship it, then keep it observable.
diff --git a/web/content/docs/agents/cli.mdx b/web/content/docs/agents/cli.mdx
new file mode 100644
index 0000000..b5169d9
--- /dev/null
+++ b/web/content/docs/agents/cli.mdx
@@ -0,0 +1,63 @@
+---
+title: 'CLI'
+description: 'The agentworkforce command surface — deploy, run locally, trigger, inspect, and tear down.'
+---
+
+Everything an agent needs after it's written. Anything in the [gallery](/agents) can also be deployed from the web with **Launch agent** — same result, no terminal.
+
+```bash
+npx agentworkforce login # browser OAuth, writes the workspace token
+npx agentworkforce logout
+```
+
+## Deploy
+
+```bash
+npx agentworkforce deploy [flags]
+```
+
+| Flag | What it does |
+| --- | --- |
+| `--mode dev \| sandbox \| cloud` | Local run, Daytona sandbox, or managed cloud. |
+| `--dry-run` | Validate with no side effects. Prints integration and schedule counts. |
+| `--on-exists update` | Redeploy over an existing persona. **Default is `cancel`** — a silent no-op. |
+| `--input KEY=value` | Override a declared input. Repeatable. |
+| `--reconnect ` | Force a fresh provider connect. |
+| `--no-connect` | Fail instead of prompting for an unconnected provider. |
+| `--workspace ` | Target a specific workspace. |
+| `--detach` | Background the runner. |
+| `--bundle-out ` | Write the deploy bundle to disk. |
+
+Deploy opens a connect flow for any declared provider that isn't connected. An unconnected provider means its triggers never fire.
+
+## Run and test locally
+
+```bash
+npx agentworkforce agent # interactive local session
+npx agentworkforce invoke --schedule daily # fire a named schedule
+npx agentworkforce invoke --fixture ev.json
+npx agentworkforce invoke --scaffold slack.message.created
+npx agentworkforce local-surface # real webhooks, local handler
+```
+
+`invoke` emits a run record. `--reads fixtures|live` and `--model stub|fixture|live` control how much of the real world it touches; `--case ` runs a YAML case with assertions.
+
+## Operate
+
+```bash
+npx agentworkforce deployments list # what's running
+npx agentworkforce deployments logs # --tail defaults to 50
+npx agentworkforce trigger # fire an active agent now
+npx agentworkforce runs export # replay fixture for a cloud run
+npx agentworkforce destroy # cancel schedules, mark destroyed
+```
+
+`deployments list` accepts `--status`, `--persona `, and `--json`. `deployments logs` with no agent lists the available workspace log files.
+
+## Promote slowly
+
+Start report-only and graduate one permission at a time: comment, label, open a PR, merge, deploy. An unattended agent should leave enough in Relay that a teammate can answer why it woke up, what it inspected, what it changed, and what needs review.
+
+
+ Tighten the persona contract before increasing autonomy.
+
diff --git a/web/content/docs/agents/deploy.mdx b/web/content/docs/agents/deploy.mdx
deleted file mode 100644
index 9d6ce43..0000000
--- a/web/content/docs/agents/deploy.mdx
+++ /dev/null
@@ -1,33 +0,0 @@
----
-title: 'Deploy and operate'
-description: 'Run proactive agents with clear triggers, scoped credentials, observable output, and human approval where it matters.'
----
-
-Deployment is where an agent becomes part of the team. Treat it like a small service with a narrower blast radius and a louder paper trail.
-
-## Before enabling a trigger
-
-- Scope credentials to the paths, repos, channels, and providers the agent actually needs.
-- Set a clear reporting channel or thread.
-- Decide which actions require approval.
-- Put a spending or runtime limit around open-ended work.
-- Test with a real event and confirm the final report is useful.
-
-## Operating loop
-
-An unattended agent should leave enough evidence that a teammate can answer:
-
-1. Why did it wake up?
-2. What context did it inspect?
-3. What did it change?
-4. What does a human need to review?
-
-If those answers are not visible in Relay, the agent is too quiet.
-
-## Promote slowly
-
-Start in report-only mode. Let the agent recommend the action, then graduate one permission at a time: comment, label, open a PR, merge, deploy.
-
-
- Design the agent contract before increasing autonomy.
-
diff --git a/web/content/docs/agents/introduction.mdx b/web/content/docs/agents/introduction.mdx
index 8f5a8d2..1f065fd 100644
--- a/web/content/docs/agents/introduction.mdx
+++ b/web/content/docs/agents/introduction.mdx
@@ -1,11 +1,89 @@
---
title: 'Agents'
-description: 'Proactive workers that watch the systems your team already uses, decide when action is needed, and report back through Relay.'
+description: 'Proactive workers that watch the systems your team already uses, decide when action is needed, and report back.'
---
-Agents turn Relay from a coordination layer into an operating system for work. They can watch channels, files, provider events, schedules, and pull requests, then take a bounded action without waiting for someone to paste context into a chat box.
+Relay agents are your team of proactive agents. Built on top of [file](/docs/file) they can react to any signal and take action. That signal can be any third party event, a schedule, or a message from another agent.
-The important part is not autonomy for its own sake. A useful agent has a small job, clear inputs, visible output, and a human-friendly trail of what it did.
+A relay agent is specialized and focused agent that has a task or a few small tasks and that it does very well. They are completely customizable
+and live in your codebase and deployable by a simple CLI command or a one click deploy on the web. You can deploy several
+relay agents and each agent on the relay can communicate with each other enabling as complex of workflows as you want with
+full traceability and control in straightforward and concise TypeScript code.
+
+## A whole agent
+
+This is [`granola-prospect`](https://github.com/AgentWorkforce/agents/tree/main/granola). A meeting recording syncs in; it decides whether the call contained a real feature ask, files a Linear issue, and hands the implementation to a coding agent that opens the PR.
+
+The persona declares what it can reach:
+
+```ts
+export default definePersona({
+ id: 'granola-prospect',
+ description: 'Detects prospect calls, files a Linear issue with the ask, and opens a PR implementing it.',
+ cloud: true,
+ useSubscription: true,
+ integrations: {
+ granola: {},
+ linear: { scope: { issues: '/linear/issues/**', teams: '/linear/teams/**' } },
+ github: { scope: { repo: 'your-org/your-repo' } }
+ },
+ harness: 'claude',
+ model: 'claude-sonnet-4-6',
+ systemPrompt: 'Turn prospect asks from meeting transcripts into a Linear issue and a small implementing PR.',
+ harnessSettings: { reasoning: 'high', timeoutSeconds: 1800, sandboxMode: 'workspace-write' },
+ onEvent: './agent.ts'
+});
+```
+
+And the handler decides what to do:
+
+```ts
+export default defineAgent({
+ triggers: { granola: [{ on: 'file.created' }] },
+ handler: async (ctx, event) => {
+ const notePath = readNotePath((await event.expand('full')).data);
+ const transcript = await readNote(ctx, notePath);
+ if (!transcript) return;
+
+ // The judgment call: was this a prospect asking for something?
+ const ask = await classify(ctx, transcript);
+ if (!ask.isProspect) return;
+
+ const linear = relayClient('linear');
+ const issue = await linear.write('issues', {}, {
+ teamId: await resolveTeamId(ctx),
+ title: ask.title,
+ description: ask.summary
+ });
+
+ // Hand the actual work to a coding agent. The repo is already checked out.
+ const run = await ctx.harness.run({
+ cwd: ctx.sandbox.cwd,
+ prompt: `A prospect asked for the following. Implement it, then open a pull request.
+ Put the PR URL on the last line.\n\n${ask.summary}`
+ });
+
+ const prUrl = run.output.match(/https?:\/\/\S*\/pull\/\d+/g)?.pop();
+ if (prUrl) {
+ await linear.write('comments', { issueId: issue.receipt?.id }, { body: `:rocket: ${prUrl}` });
+ }
+ }
+});
+```
+
+`classify` is a model call, not a rule:
+
+```ts
+const ask = await ctx.llm.complete(`
+ Read this meeting transcript. Decide if it is a sales/prospect call where the
+ prospect asked for a feature or change. Reply with JSON only:
+ {"isProspect": boolean, "title": "short issue title", "summary": "what they asked for"}
+
+ ${transcript.slice(0, 8000)}
+`, { maxTokens: 400 });
+```
+
+The agent makes decisions based on the inputs and allows the code to lean but accomplish complicated functionality.
## What an agent needs
@@ -14,19 +92,17 @@ The important part is not autonomy for its own sake. A useful agent has a small
- **A policy**: what the agent may read, write, spend, and deploy.
- **A reporting path**: where the agent posts progress, findings, and handoffs.
-Relay gives that work a shared substrate: messages for coordination, Relayfile for provider-backed state, and profiles for repeatable behavior.
-
## Start here
- Launch a small repo-review agent and see how it reports through Relay.
-
-
- Pick the shape that matches the job: reviewer, monitor, triager, or worker pair.
+ Get an agent from the gallery running in one click.
- Define the trigger, behavior, permissions, and handoff path for a custom agent.
+ Author a cloud persona: `persona.json` for wiring, `agent.ts` for behavior.
+
+
+ Deploy, test locally, trigger, read logs, and tear down.
diff --git a/web/content/docs/agents/patterns.mdx b/web/content/docs/agents/patterns.mdx
deleted file mode 100644
index 7a9dcd9..0000000
--- a/web/content/docs/agents/patterns.mdx
+++ /dev/null
@@ -1,34 +0,0 @@
----
-title: 'Agent patterns'
-description: 'Use a small set of repeatable patterns for proactive agents instead of designing every workflow from scratch.'
----
-
-Good agents are boring in the best way. They have one reason to wake up, one definition of done, and a clear path back to the team.
-
-## Reviewer
-
-Reviews pull requests, migrations, release notes, or generated work. The agent should cite files, produce concrete findings, and hand back a short pass/fail summary.
-
-Use it when quality gates are frequent and repetitive.
-
-## Monitor
-
-Watches outside signals: package releases, vendor changelogs, HN keywords, inbound emails, or provider webhooks. It should dedupe aggressively and only post when the signal clears a threshold.
-
-Use it when the job is mostly "notice the important thing."
-
-## Triager
-
-Turns unstructured input into routed work. It can classify Linear issues, summarize customer requests, open follow-up tickets, or assign an owner.
-
-Use it when the next step is known, but humans spend time sorting the queue.
-
-## Worker pair
-
-Splits a job between an implementer and a reviewer. One agent changes the system, the other checks the result and asks for corrections before a human sees it.
-
-Use it when the task benefits from independent review before handoff.
-
-
- See concrete proactive-agent use cases.
-
diff --git a/web/content/docs/agents/quickstart.mdx b/web/content/docs/agents/quickstart.mdx
index 76207e2..92c5127 100644
--- a/web/content/docs/agents/quickstart.mdx
+++ b/web/content/docs/agents/quickstart.mdx
@@ -1,50 +1,35 @@
---
title: 'Quickstart'
-description: 'Fork an existing proactive agent, connect it to a Relay workspace, and run it against a real event.'
+description: 'Get a proactive agent running in one click.'
---
-The fastest way to start is to fork an existing agent and keep the first job narrow. Review a PR. Triage one Linear project. Watch one package. Send one digest.
+Start with someone else's agent. Pick one from the [gallery](/agents) and hit **Launch agent**.
-## 1. Pick an agent
+The deploy wizard opens with that agent's persona already loaded. Pick a workspace, connect the providers it asks for, choose how it gets model access, fill in its inputs — Slack channel, repo, whatever it declared — and deploy.
-Start from the open-source agents repo:
+That's it. Its triggers and schedules are registered and it's live. Sandboxes spin up on the first trigger fire, so an idle agent costs nothing.
-```bash
-gh repo clone AgentWorkforce/agents
-cd agents
-```
-
-Choose a small agent directory as the reference. Each agent should make the trigger, persona, and implementation obvious from the files in that folder.
-
-## 2. Connect Relay
-
-Install the Relay skill in the coding harness you use for setup work:
+## Prefer the terminal?
```bash
-npx @agent-relay@latest skills add
+gh repo clone AgentWorkforce/agents && cd agents
+npx agentworkforce login
+npx agentworkforce deploy ./hn-monitor/persona.json --mode cloud
```
-Then create or select the workspace where the agent should report progress. The same workspace can host the agent's channel, human handoffs, and durable context.
-
-## 3. Run the first event
-
-Use a real event, but keep the permission surface small:
+Then fire it without waiting for its schedule:
```bash
-agent-relay spawn repo-reviewer \
- --channel dev \
- --task "Review the latest pull request and report findings."
+npx agentworkforce trigger hn-monitor
```
-The first successful run should prove three things: the agent got the right context, stayed inside its policy, and reported its work somewhere humans already look.
-
## Next
-
- Use a known shape instead of inventing every workflow from scratch.
+
+ Write a persona from scratch — or have your coding agent write it.
-
- Add schedules, secrets, limits, and reporting before leaving it unattended.
+
+ Deploy, test locally, trigger, read logs, and tear down.
diff --git a/web/lib/agents.ts b/web/lib/agents.ts
index d48636b..4068169 100644
--- a/web/lib/agents.ts
+++ b/web/lib/agents.ts
@@ -199,9 +199,11 @@ export const AGENTS: Agent[] = [
summary: 'Twice a day · 9am & 5pm ET',
detail: '0 9,17 * * *',
},
- integrations: ['slack', 'hacker-news'],
+ // slack and telegram are both optional transports gated by
+ // enabledByInput (SLACK_CHANNEL / TELEGRAM_CHAT) — set either or both.
+ integrations: ['slack', 'telegram', 'hacker-news'],
runtime: 'Claude · claude-haiku-4-5',
- inputs: ['TOPICS', 'SLACK_CHANNEL'],
+ inputs: ['TOPICS', 'SLACK_CHANNEL', 'TELEGRAM_CHAT'],
accent: '#c1674b',
hasCustomArt: true,
},
@@ -249,9 +251,11 @@ export const AGENTS: Agent[] = [
summary: 'Every day · 10am ET',
detail: '0 10 * * *',
},
- integrations: ['spotify', 'slack'],
+ // Delivery over Slack DM, Telegram, or both — each optional and gated by
+ // enabledByInput (SLACK_USER / TELEGRAM_CHAT).
+ integrations: ['spotify', 'slack', 'telegram'],
runtime: 'Default harness',
- inputs: ['SLACK_USER', 'SPOTIFY_TOKEN'],
+ inputs: ['SLACK_USER', 'TELEGRAM_CHAT', 'SPOTIFY_TOKEN'],
accent: '#1db954',
hasCustomArt: true,
},
@@ -610,6 +614,32 @@ export function agentAsset(slug: string, asset: 'banner' | 'card' | 'card-sm'):
return `/agent-art/${slug}/${asset}.png`;
}
+/**
+ * Provider logos vendored from Nango's template logos by
+ * scripts/sync-integration-logos.sh — the same source pear resolves icons from.
+ * Nango has no logo for every provider we support (it answers 200 with its app
+ * shell for unknown slugs), so this maps only the ones that resolve to a real
+ * image. Anything absent renders as a text chip instead; re-run the sync script
+ * after adding an integration and add it here only if a file lands.
+ */
+const INTEGRATION_LOGO_FILES: Partial> = {
+ cloudflare: 'cloudflare.svg',
+ github: 'github.svg',
+ 'google-mail': 'google-mail.svg',
+ granola: 'granola.png',
+ linear: 'linear.svg',
+ notion: 'notion.svg',
+ slack: 'slack.svg',
+ spotify: 'spotify.svg',
+ telegram: 'telegram.svg',
+};
+
+/** Logo URL for an integration, or undefined when no vendored logo exists. */
+export function integrationLogo(integration: Integration): string | undefined {
+ const file = INTEGRATION_LOGO_FILES[integration];
+ return file ? `/integration-logos/${file}` : undefined;
+}
+
function agentRepo(agent: Agent): string {
return agent.repo ?? DEFAULT_REPO;
}
diff --git a/web/lib/product-docs-nav.ts b/web/lib/product-docs-nav.ts
index 0d2991c..3b01858 100644
--- a/web/lib/product-docs-nav.ts
+++ b/web/lib/product-docs-nav.ts
@@ -151,14 +151,11 @@ export const agentsSection: ProductDocSection = {
},
{
title: 'Design',
- items: [
- { title: 'Agent patterns', slug: 'patterns' },
- { title: 'Build your own', slug: 'build' },
- ],
+ items: [{ title: 'Build your own', slug: 'build' }],
},
{
title: 'Run',
- items: [{ title: 'Deploy and operate', slug: 'deploy' }],
+ items: [{ title: 'CLI', slug: 'cli' }],
},
],
};
diff --git a/web/next.config.mjs b/web/next.config.mjs
index 2be9a29..08fe1c1 100644
--- a/web/next.config.mjs
+++ b/web/next.config.mjs
@@ -81,6 +81,11 @@ const nextConfig = {
{ source: '/relaycast/:path*', destination: '/primitives#message', permanent: true },
{ source: '/docs/reference-sdk', destination: '/docs/typescript-sdk', permanent: true },
{ source: '/docs/reference-sdk-py', destination: '/docs/typescript-sdk', permanent: true },
+ // The agents docs lost these two pages: 'Deploy and operate' became the
+ // CLI reference, and 'Agent patterns' was dropped without a successor
+ // (the build guide is the nearest thing that still covers agent shape).
+ { source: '/docs/agents/deploy', destination: '/docs/agents/cli', permanent: true },
+ { source: '/docs/agents/patterns', destination: '/docs/agents/build', permanent: true },
];
},
};
diff --git a/web/public/integration-logos/cloudflare.svg b/web/public/integration-logos/cloudflare.svg
new file mode 100644
index 0000000..2c84b33
--- /dev/null
+++ b/web/public/integration-logos/cloudflare.svg
@@ -0,0 +1 @@
+
diff --git a/web/public/integration-logos/github.svg b/web/public/integration-logos/github.svg
new file mode 100644
index 0000000..b7c3ee7
--- /dev/null
+++ b/web/public/integration-logos/github.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/public/integration-logos/google-mail.svg b/web/public/integration-logos/google-mail.svg
new file mode 100644
index 0000000..2136fcd
--- /dev/null
+++ b/web/public/integration-logos/google-mail.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/public/integration-logos/granola.png b/web/public/integration-logos/granola.png
new file mode 100644
index 0000000..6216a09
Binary files /dev/null and b/web/public/integration-logos/granola.png differ
diff --git a/web/public/integration-logos/linear.svg b/web/public/integration-logos/linear.svg
new file mode 100644
index 0000000..c89d91c
--- /dev/null
+++ b/web/public/integration-logos/linear.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/public/integration-logos/notion.svg b/web/public/integration-logos/notion.svg
new file mode 100644
index 0000000..b685add
--- /dev/null
+++ b/web/public/integration-logos/notion.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/public/integration-logos/slack.svg b/web/public/integration-logos/slack.svg
new file mode 100644
index 0000000..ae95ae0
--- /dev/null
+++ b/web/public/integration-logos/slack.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/public/integration-logos/spotify.svg b/web/public/integration-logos/spotify.svg
new file mode 100644
index 0000000..097076b
--- /dev/null
+++ b/web/public/integration-logos/spotify.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/public/integration-logos/telegram.svg b/web/public/integration-logos/telegram.svg
new file mode 100644
index 0000000..d2ecb2c
--- /dev/null
+++ b/web/public/integration-logos/telegram.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/web/scripts/sync-integration-logos.sh b/web/scripts/sync-integration-logos.sh
new file mode 100755
index 0000000..de04751
--- /dev/null
+++ b/web/scripts/sync-integration-logos.sh
@@ -0,0 +1,98 @@
+#!/usr/bin/env bash
+#
+# Vendor the integration provider logos into public/integration-logos/ so the
+# /agents gallery and agent detail pages serve them as static assets instead of
+# hotlinking Nango on every render.
+#
+# Source of truth: Nango's template logos, the same convention pear uses to
+# resolve a provider icon (see pear/src/main/integrations.ts nangoTemplateLogoUrl).
+#
+# Two things this script has to defend against:
+#
+# 1. Nango answers 200 with its SPA HTML shell for slugs it has no logo for,
+# so a status check is not enough — we sniff the body for an SVG root and
+# skip anything else. As of this writing daytona, gcp, hacker-news, neon,
+# and npm have no logo and intentionally fall through to a text chip in
+# the UI (see INTEGRATION_LOGO_FILES in lib/agents.ts).
+# 2. granola's logo is a ~525KB SVG wrapping a base64 PNG — absurd for a 16px
+# mark — so it is unwrapped and downscaled to a small PNG instead.
+#
+# Re-running is safe and idempotent. After adding an integration, run this and
+# then add it to INTEGRATION_LOGO_FILES only if a file actually lands here.
+#
+# NOTE: destination is public/integration-logos, NOT public/integrations —
+# top-level public/ folders become CloudFront → S3 behaviors under SST/OpenNext
+# and would shadow any same-named page route (see sync-agent-assets.sh).
+#
+# Usage: web/scripts/sync-integration-logos.sh
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+WEB_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
+DEST="$WEB_DIR/public/integration-logos"
+BASE="https://app.nango.dev/images/template-logos"
+TMP="$(mktemp -d)"
+trap 'rm -rf "$TMP"' EXIT
+
+# Every provider in the `Integration` union in lib/agents.ts. Slugs that Nango
+# has no logo for are listed anyway so the skip is visible in the output.
+SLUGS=(
+ cloudflare
+ daytona
+ gcp
+ github
+ google-mail
+ granola
+ hacker-news
+ linear
+ neon
+ notion
+ npm
+ slack
+ spotify
+ telegram
+)
+
+mkdir -p "$DEST"
+echo "Syncing integration logos from $BASE -> $DEST"
+
+for slug in "${SLUGS[@]}"; do
+ raw="$TMP/$slug.svg"
+
+ if ! curl -sfL "$BASE/$slug.svg" -o "$raw"; then
+ echo " skip $slug (fetch failed)"
+ continue
+ fi
+
+ # Nango serves its app shell for unknown slugs — only keep real SVGs.
+ if ! head -c 512 "$raw" | grep -qi '