# Flue examples

> Copy-pasteable fixtures for hello-world workflows, channel ingress, observability adapters, and target-specific integrations.

- Repository: withastro/flue-with-vercel-eve
- GitHub: https://github.com/withastro/flue
- Human docs: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6
- Complete Markdown: https://grok-wiki.com/public/docs/withastro-flue-with-vercel-eve-f4b79875fff6/llms-full.txt

## Source Files

- `withastro-flue:examples/hello-world/src/workflows/hello.ts`
- `withastro-flue:examples/slack-channel/src/channels/slack.ts`
- `withastro-flue:examples/braintrust/README.md`
- `withastro-flue:examples/cloudflare/src/workflows/skills-from-r2.ts`
- `withastro-flue:examples/react-chat/flue.config.ts`
- `withastro-flue:examples/notion-channel/README.md`
- `withastro-flue:examples/imported-skill/README.md`

---

---
title: "Flue examples"
description: "Copy-pasteable fixtures for hello-world workflows, channel ingress, observability adapters, and target-specific integrations."
---

The `withastro-flue/examples/` directory ships 25 self-contained fixtures. Each example is a private workspace package with its own `package.json`, `flue.config.ts` (where needed), and `src/` tree. Fixtures demonstrate one capability end-to-end so you can copy the pattern into a production app without importing example code as a library.

## Example catalog

| Directory | Category | Primary surface |
| --- | --- | --- |
| `hello-world` | Workflow baseline | Prompts, skills, tools, sandboxes, providers |
| `imported-skill` | Skills and sandboxes | `import ... with { type: 'skill' }`, custom `just-bash` |
| `cloudflare` | Cloudflare target | Workers AI binding, R2/git hydration, cf-shell |
| `react-chat` | UI integration | `@flue/react` hooks + Vite static client |
| `braintrust` | Observability | `observe(...)` → Braintrust traces |
| `sentry` | Observability | `observe(...)` → Sentry error captures |
| `chat-sdk` | Channel + SDK | Chat SDK GitHub adapter + `dispatch` |
| `slack-channel`, `notion-channel`, `github-channel`, … | Channel ingress | Verified webhooks at `/channels/:name/*` |
| `assistant` | Deploy fixture | Dockerfile + `wrangler.jsonc` for agent hosting |

Channel examples follow the same project-owned shape: a `src/channels/<provider>.ts` module exports `channel` (ingress) and project-owned SDK clients/tools; `src/agents/assistant.ts` binds outbound tools to the conversation key parsed from the dispatch `id`. Provider packages (`@flue/slack`, `@flue/notion`, `@flue/github`, …) supply signature verification and route mounting — not application policy tools.

<Note>
Build `@flue/runtime` and `@flue/cli` before running examples from a fresh checkout. Most examples declare both as workspace dependencies and consume the local `dist/` artifacts produced by `pnpm run build`.
</Note>

## Hello-world workflows

`examples/hello-world/` is the general runtime integration fixture referenced in the repository `AGENTS.md`. It exports many workflow modules under `src/workflows/` and composes a custom Hono `app.ts` that registers local OpenAI-compatible providers and mounts `flue()` at `/`.

### Minimal prompt workflow

`hello.ts` is the smallest runnable workflow: create an agent, initialize a harness, open a session, prompt with a Valibot `result` schema, and read a workspace file via `session.shell`.

```ts
const agent = createAgent(() => ({ model: 'anthropic/claude-sonnet-4-6' }));

export async function run({ init, log }: FlueContext) {
  const harness = await init(agent);
  const session = await harness.session();
  const response = await session.prompt('What is 2 + 2? Return only the number.', {
    result: v.object({ answer: v.number() }),
  });
  log.info('solved arithmetic prompt', { answer: response.data.answer });
  return response.data;
}
```

<Steps>
<Step title="Run the hello workflow">

From `examples/hello-world/`:

```bash
pnpm exec flue run hello --target node --env ../../.env
```

</Step>
<Step title="Verify structured output">

The workflow returns `{ answer: 4 }` and logs token usage. Console output includes `[hello] 2 + 2 =` and a `cat AGENTS.md` read via shell.

</Step>
</Steps>

### Workflow modules in the same fixture

| Workflow | Demonstrates |
| --- | --- |
| `with-skill` | `session.skill('greet', { args, result })` against `.agents/skills/greet/SKILL.md` |
| `with-tools` | `defineTool` passed to `session.prompt`, plus built-in `task` delegation |
| `with-sandbox` | Daytona and other sandbox adapters |
| `with-subagent` | Delegated agent sessions |
| `with-registered-provider` | `registerProvider` for Ollama/LM Studio gateways |
| `with-request` | HTTP request context inside `run()` |
| `compaction-test` | Context compaction boundaries |
| `fs-test`, `fs-surface-test` | Filesystem surfaces |

The `greet` skill lives at `.agents/skills/greet/SKILL.md` with frontmatter `name: greet`. Flue discovers it during `init()` from the standard skills lookup path.

### Custom `app.ts` composition

`hello-world/src/app.ts` shows how runtime concerns stay out of `flue.config.ts`: provider registration (`registerProvider('ollama', …)`), plain Hono middleware, a custom `/api/ping` route, and `app.route('/', flue())` for the built-in agent API.

## Channel ingress fixtures

Channel examples receive verified provider webhooks, derive a canonical conversation instance id, call `dispatch(agent, { id, input })`, and define narrow outbound tools that close over trusted context (thread id, page id, issue number). Agents are intentionally dispatch-only; direct `/agents/:name/:id` routes would need separate authorization.

### Slack (`slack-channel`)

Ingress route: `POST /channels/slack/events`.

<ParamField body="SLACK_SIGNING_SECRET" type="string" required>
Verifies Events API signatures. Requests older than five minutes are rejected.
</ParamField>

<ParamField body="SLACK_BOT_TOKEN" type="string" required>
Powers the project-owned `WebClient` used by `reply_in_slack_thread`.
</ParamField>

```ts
export const channel = createSlackChannel({
  signingSecret: requiredEnv('SLACK_SIGNING_SECRET'),
  async events({ payload }) {
    if (payload.type !== 'event_callback') return;
    if (payload.event.type === 'app_mention') {
      const thread = { teamId: payload.team_id, channelId: event.channel, threadTs: event.thread_ts ?? event.ts };
      await dispatch(assistant, {
        id: channel.conversationKey(thread),
        input: { type: 'slack.app_mention', eventId: payload.event_id, text: event.text },
      });
    }
  },
});
```

The agent binds `replyInThread(channel.parseConversationKey(id))` as a tool. Optional `/channels/slack/interactions` and `/channels/slack/commands` surfaces are shown commented in the channel module.

<Warning>
Handlers must complete dispatch admission before acknowledging Slack. Slack retries slow or failed Events API deliveries — claim `payload.event_id` in durable storage when uniqueness matters.
</Warning>

### Notion (`notion-channel`)

Ingress route: `POST /channels/notion/webhook`.

<ParamField body="NOTION_TOKEN" type="string" required>
Powers the official `@notionhq/client` v5.22.0 client with injected Fetch transport.
</ParamField>

<ParamField body="NOTION_WEBHOOK_VERIFICATION_TOKEN" type="string">
Set after initial endpoint verification. While unset, uncomment the temporary `verification(...)` callback in the channel module.
</ParamField>

Page lifecycle events (`page.created`, `page.content_updated`, …) dispatch to instance ids with the local `notion-page:` prefix. The `retrieve_notion_page` tool closes over the page id in application code — the model cannot choose another page.

`page.deleted` is intentionally ignored because the retrieval tool cannot read deleted pages.

### GitHub (`github-channel`)

Ingress route: `POST /channels/github/webhook`. Requires `GITHUB_WEBHOOK_SECRET` and `GITHUB_TOKEN`. Webhook content type must be `application/json`. The handler returns `2xx` within ten seconds; GitHub does not auto-retry failed deliveries.

### Other channel fixtures

The same dispatch + project-owned tool pattern appears in `discord-channel`, `telegram-channel`, `teams-channel`, `google-chat-channel`, `linear-channel`, `messenger-channel`, `stripe-channel`, `shopify-channel`, `twilio-channel`, `whatsapp-channel`, `zendesk-channel`, `intercom-channel`, `resend-channel`, and `salesforce-marketing-cloud-channel`. Each README documents required env vars, route paths, and workerd test commands (`pnpm run check:types`, `pnpm run test`, `pnpm run build`, `pnpm run build:cloudflare`).

## Observability adapters

Both observability examples register a global `observe(...)` subscriber in `src/app.ts`. Workflows do not import Braintrust or Sentry.

### Braintrust tracing (`braintrust`)

When `BRAINTRUST_API_KEY` is set, `initLogger` runs and `braintrustFlueObserver` receives compatible `FlueEvent` shapes:

| Flue event | Bridge behavior |
| --- | --- |
| `run_start`, `run_end` | Tracked in `observedRuns` set |
| `tool` | Renamed to `tool_call` for Braintrust 3.17 |
| `run_resume` | Synthesized as `run_start` when the isolate missed the original start |
| `operation_*`, `turn_*`, `task_*`, `compaction_*` | Forwarded as-is |

Trace shape for a tool-using workflow:

```text
workflow:tools
  flue.prompt
    llm:<model>
    tool:lookup_weather
    llm:<model>
```

<Info>
When `BRAINTRUST_API_KEY` is absent, the app runs unchanged with no trace export. Braintrust's observer is content-bearing — review masking and retention before enabling on sensitive workloads.
</Info>

Trigger example workflows after `pnpm exec flue dev`:

```bash
curl -X POST 'http://localhost:3583/workflows/prompt?wait=result' \
  -H 'content-type: application/json' -d '{"name":"Developer"}'
```

### Sentry error reporting (`sentry`)

`Sentry.init` runs at module scope with `enabled: Boolean(process.env.SENTRY_DSN)`. The bridge captures:

| Flue event | Sentry call |
| --- | --- |
| `run_end` with `isError: true` | `captureException` with `flue.run.id` tag |
| `log` with `level: 'error'` and `attributes.error` | `captureException` |
| `log` with `level: 'error'` without `error` attribute | `captureMessage` |

Pivot on `flue.run.id` in Sentry, then replay the run:

```bash
flue logs run_01HX...
```

<Note>
On Cloudflare, each Durable Object is its own V8 isolate. `app.ts` (and thus `observe`) is evaluated per isolate — each agent reports errors independently. This is the intended shape, not a workaround.
</Note>

Deliberate scope cuts: no spans/traces, no `log.info`/`log.warn` forwarding, no tool-error capture, no AI metrics forwarding.

## Target-specific integrations

### Cloudflare (`cloudflare`)

`flue dev --target cloudflare` serves three workflow/agent surfaces:

| Module | Demonstrates |
| --- | --- |
| `with-cloudflare-binding` | Workers AI binding routing (no API keys) |
| `skills-from-r2` | `hydrateFromBucket` → discovered skill from R2 |
| `skills-from-git` | `createGit` clone into cf-shell `Workspace` |

R2 hydration copies bucket keys verbatim (default `prefix: ''`). Objects under `.agents/skills/<name>/SKILL.md` become registered skills. A `/.hydrated` sentinel short-circuits repeat hydration; bump the sentinel key to force re-hydration after bucket changes.

<Warning>
`skills-from-r2` and `skills-from-git` require a `worker_loaders` binding (beta). Local `wrangler dev` may not expose R2 to the running server — use `wrangler dev --remote` or deploy to a preview environment. Seed R2 with `./seed-r2.sh` before running `skills-from-r2`.
</Warning>

```bash
curl -X POST http://localhost:3583/workflows/skills-from-r2?wait=result \
  -H 'Content-Type: application/json' -d '{}'
```

The cf-shell sandbox adapter lives at `src/sandboxes/cloudflare-shell.ts` (conceptually generated by `flue add sandbox @cloudflare/shell`).

### React chat UI (`react-chat`)

`react-chat` composes a Vite-built React client with Flue's HTTP API:

```ts
registerProvider('react-chat-example', { api: 'react-chat-example', baseUrl: '' });

const app = new Hono();
app.route('/api', flue());
app.use('*', serveStatic({ root: './dist/client' }));
```

`flue.config.ts` pins `target: 'node'` and `output: 'dist/server'`. The UI uses `@flue/react` hooks (`useFlueAgent`, `useFlueWorkflow`, `useFlueClient`) against `createFlueClient({ baseUrl: '/api' })`.

<Steps>
<Step title="Build UI and start dev server">

```bash
pnpm run dev   # runs build:ui then flue dev
```

</Step>
<Step title="Exercise agent chat and workflow streaming">

Open the served client. Agent messages stream via `useFlueAgent`; the demo workflow logs appear via `useFlueWorkflow({ runId })`.

</Step>
</Steps>

### Chat SDK + GitHub (`chat-sdk`)

`chat-sdk` wires Chat SDK's GitHub adapter for bidirectional issue-comment messaging while Flue owns agent execution. The fixture uses in-memory Chat SDK state and a scripted model provider for deterministic local e2e tests.

```txt
signed GitHub issue_comment webhook
  -> Chat SDK GitHub adapter
  -> dispatch(assistant, ...)
  -> Flue agent tool
  -> bot.thread(threadId).post(...)
```

## Skills and custom sandboxes (`imported-skill`)

Two workflow contracts ship in one fixture:

**`with-imported-skill`** — imports `src/skills/review/SKILL.md` with `{ type: 'skill' }`. The import is a lightweight `SkillReference`; the complete skill directory (including `CHECKLIST.txt`) is packaged. Register the reference in `skills: [review]` so files are readable during `session.skill(review)`.

```ts
import review from '../skills/review/SKILL.md' with { type: 'skill' };

const agent = createAgent(() => ({ model: 'anthropic/claude-haiku-4-5', skills: [review] }));
```

**`with-custom-bash`** — imports `Bash` and `InMemoryFs` directly from `just-bash` (declared in app dependencies) and passes `bash(() => new Bash({ fs }))` as the sandbox.

<CodeGroup>
```bash title="Imported skill (Node)"
pnpm exec flue run with-imported-skill --target node --env ../../.env
```

```bash title="Custom bash (Node or Cloudflare)"
pnpm exec flue run with-custom-bash --target node
pnpm exec flue dev --target cloudflare
```
</CodeGroup>

## Shared layout pattern

Most examples follow the same source-root layout:

:::files
examples/<name>/
├── flue.config.ts          # target, root, output (build-time only)
├── package.json
├── tsconfig.json
├── AGENTS.md               # optional system prompt for init()
└── src/
    ├── app.ts              # Hono composition, providers, observe hooks
    ├── agents/
    │   └── assistant.ts    # createAgent default export
    ├── workflows/          # run() + route exports
    ├── channels/           # channel ingress (channel examples)
    └── sandboxes/          # project-owned adapters (cloudflare, hello-world)
:::

<ParamField body="target" type="'node' | 'cloudflare'">
Selects runtime adapter. Omit in `flue.config.ts` when relying on `--target` CLI overrides (as `hello-world` does).
</ParamField>

<ParamField body="root" type="string">
Source root for agent/workflow discovery. Defaults to `src/` or `.flue/` per project layout rules.
</ParamField>

Provider and model registration intentionally live in `app.ts` via `registerProvider(...)`, not in `flue.config.ts`, because API keys often come from `process.env` or Cloudflare bindings at runtime.

## Verification commands

| Example type | Typical checks |
| --- | --- |
| Workflow baseline | `pnpm exec flue run <workflow> --target node` |
| Channel ingress | `pnpm run check:types && pnpm run test && pnpm run build` |
| Cloudflare target | `pnpm exec flue dev --target cloudflare` or `wrangler dev --remote` |
| Observability | `curl -X POST http://localhost:3583/workflows/<name>?wait=result` then inspect vendor UI |
| React UI | `pnpm run dev` then exercise browser client |

HTTP triggers use port `3583` by default (`flue dev`). Responses include a top-level `runId` for workflow invocations.

## Related pages

<CardGroup>
<Card title="Flue quickstart" href="/flue-quickstart">
Scaffold a project, run `flue dev`, invoke workflows with `flue run`, and connect to an agent instance.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Define workflow modules, initialize agents inside `run()`, and inspect run events with `flue logs`.
</Card>
<Card title="Flue channels" href="/flue-channels">
Discover channel modules, mount verified webhook routes, and dispatch inbound events to agents.
</Card>
<Card title="Flue deploy" href="/flue-deploy">
Choose Node or Cloudflare targets, build artifacts with `flue build`, and apply blueprint-driven integrations.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
Author agents with `createAgent`, tools, skills, sandboxes, and providers.
</Card>
<Card title="Flue project layout" href="/flue-project-layout">
Source-root resolution, agent and workflow modules, and `app.ts` composition boundaries.
</Card>
</CardGroup>
