# Flue project layout

> Source-root resolution (`.flue/` vs `src/`), agent and workflow modules, `app.ts` composition, and discovery boundaries.

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

## Source Files

- `withastro-flue:AGENTS.md`
- `withastro-flue:packages/cli/src/lib/source-root.ts`
- `withastro-flue:packages/cli/src/lib/config.ts`
- `withastro-flue:examples/hello-world/src/app.ts`
- `withastro-flue:examples/hello-world/src/agents/session-test.ts`
- `withastro-flue:examples/hello-world/src/workflows/hello.ts`
- `withastro-flue:knip.json`

---

---
title: "Flue project layout"
description: "Source-root resolution (`.flue/` vs `src/`), agent and workflow modules, `app.ts` composition, and discovery boundaries."
---

Flue projects are TypeScript modules under a resolved **source root** (`sourceRoot`). The CLI discovers `agents/`, `workflows/`, and `channels/` there, optionally loads `app.ts`, `db.ts`, and `cloudflare.ts`, and generates a server entry that wires those modules into `@flue/runtime`. Build-time config lives in `flue.config.*` at the project root; runtime provider registration lives in `app.ts`.

## Source root resolution

`resolveSourceRoot(root)` picks the directory where authored Flue modules live. Precedence is fixed:

| Priority | Directory | When selected |
| --- | --- | --- |
| 1 | `<root>/.flue/` | Directory exists |
| 2 | `<root>/src/` | `.flue/` absent and `src/` exists |
| 3 | `<root>/` | Neither `.flue/` nor `src/` exists |

<Warning>
When `.flue/` exists, Flue **ignores** bare `agents/` and `workflows/` at the project root. Modules in `src/` are also outside the active source root in that case.
</Warning>

The resolved path is stored as `flueConfig.sourceRoot` and passed to `flue build` and `flue dev`. Override the project root with `root` in `flue.config.ts` or `--root`; source-root selection always runs against that resolved root.

<ParamField body="root" type="string">
Absolute or relative project root. Defaults to the config file directory (or the search directory when no config is loaded). Relative values in `flue.config.*` resolve from the config directory; inline CLI values resolve from the caller's working directory.
</ParamField>

<ParamField body="output" type="string">
Build output directory. Defaults to `<root>/dist`. Must not resolve to `root` or `sourceRoot` — the CLI rejects that to avoid watcher loops and accidental source deletion.
</ParamField>

:::files
my-flue-project/
├── flue.config.ts          # build target, root, output
├── package.json
├── AGENTS.md               # runtime context (not CLI-discovered)
├── .agents/skills/         # runtime skills (not CLI-discovered)
│
├── src/                    # source root when .flue/ is absent
│   ├── app.ts              # optional HTTP composition
│   ├── db.ts               # optional persistence (Node only)
│   ├── cloudflare.ts       # optional Worker exports (Cloudflare only)
│   ├── agents/
│   │   └── session-test.ts
│   ├── workflows/
│   │   └── hello.ts
│   ├── channels/
│   │   └── slack.ts
│   └── sandboxes/          # imported by agents; not auto-discovered
│       └── daytona.ts
│
└── dist/                   # default build output
    └── server.mjs          # Node target
:::

The Flue monorepo dogfoods the `.flue/` layout: repository workflows such as `pr-redirect` live under `.flue/workflows/` while examples use `src/`.

## Discovery boundaries

Discovery runs at build time inside `sourceRoot`. The dev watcher rebuilds when files under `agents/`, `workflows/`, `channels/`, or top-level `app.*` / `cloudflare.*` change.

### Required modules

At least one agent **or** workflow file must exist. Channels alone do not satisfy the build:

```
[flue] No agent or workflow files found.
Expected at: <sourceRoot>/agents/ or <sourceRoot>/workflows/
```

### Module directories

| Directory | Filename → name | Extensions |
| --- | --- | --- |
| `agents/` | basename without extension | `.ts`, `.mts`, `.js`, `.mjs` |
| `workflows/` | basename without extension | `.ts`, `.mts`, `.js`, `.mjs` |
| `channels/` | basename without extension | `.ts`, `.mts`, `.js`, `.mjs` |

Rules enforced during discovery:

- One file per basename; duplicates throw.
- Agent and channel names must be non-empty and must **not** contain `:`.
- Workflow names must be non-empty (no `:` restriction).
- Declaration files matching `*.d.ts` / `*.d.mts` are skipped.

### Optional top-level entries

| File | Target | Role |
| --- | --- | --- |
| `app.{ts,mts,js,mjs}` | Node, Cloudflare | Custom HTTP app; extension priority `ts` > `mts` > `js` > `mjs` |
| `db.{ts,mts,js,mjs}` | Node only | Default-export `PersistenceAdapter` |
| `cloudflare.{ts,mts,js,mjs}` | Cloudflare only | Non-HTTP Worker handler exports; must not define `fetch` |

### Outside discovery

These paths are **not** scanned by the CLI but are part of typical project layout:

| Path | When used |
| --- | --- |
| `sandboxes/` | Imported by agents after `flue add` sandbox blueprints |
| `tools/`, `skills/` | Imported statically; packaged skills use `import … with { type: 'skill' }` |
| `AGENTS.md`, `.agents/skills/` | Discovered at **runtime** from the session working directory |
| `flue.config.*` | At project root, not under `sourceRoot` |

<Info>
`knip.json` in the Flue repo mirrors the same entry patterns: `.flue/{app,cloudflare,agents/*,workflows/*}` for the monorepo root and `src/{app,cloudflare,agents/*,workflows/*,channels/*}` for examples.
</Info>

## Agent modules

An agent module lives at `agents/<name>.ts`. The filename becomes the agent name exposed over HTTP and CLI (`flue connect <name>`).

**Required default export** — a value from `createAgent(...)`:

```ts title="agents/session-test.ts"
import { type AgentRouteHandler, createAgent, defineAgentProfile } from '@flue/runtime';

export const route: AgentRouteHandler = async (_c, next) => next();

const sessionTest = defineAgentProfile({
  instructions: 'You are a test agent for session-oriented message delivery.',
});

export default createAgent(() => ({ profile: sessionTest }));
```

**Optional named exports:**

| Export | Type | Effect |
| --- | --- | --- |
| `route` | `AgentRouteHandler` | Per-agent HTTP middleware before dispatch |
| `description` | non-empty `string` | Listed in agent manifest |

Validation at build time (generated entry):

- Default export must have `__flueCreatedAgent === true` and an `initialize` function.
- Two agent modules cannot default-export the same `createAgent` value.

Terminology from `AGENTS.md`: an **agent profile** is a reusable `defineAgentProfile(...)` value; a **created agent** is the `createAgent(...)` initializer; an **agent instance** gets a URL `<id>` when initialized.

## Workflow modules

A workflow module lives at `workflows/<name>.ts`. Invoke it with `flue run <name>` or `POST /workflows/:name`.

**Required export** — async `run(ctx: FlueContext)`:

```ts title="workflows/hello.ts"
import { createAgent, type FlueContext, type WorkflowRouteHandler } from '@flue/runtime';
import * as v from 'valibot';

export const route: WorkflowRouteHandler = async (_c, next) => next();

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;
}
```

**Optional named export:**

| Export | Type | Effect |
| --- | --- | --- |
| `route` | `WorkflowRouteHandler` | HTTP admission middleware; presence also enables HTTP transport in the manifest |

Workflows initialize agents locally via `init(agent)` inside `run()`. Each invocation gets a unique `ctx.id` (run ID). Runs are workflow-only — direct agent HTTP prompts and channel dispatches use sessions, not `/runs`.

## Channel modules

Channel modules live at `channels/<name>.ts` and export a named `channel` binding with a `routes` array. Routes mount under `/channels/:name` at runtime. See the channels guide for webhook verification and dispatch semantics.

Channels are discovered like agents and workflows but cannot be the sole authored module.

## `app.ts` composition

`app.ts` is optional. When present, its **default export** owns the entire HTTP pipeline; the generated entry forwards all requests to `app.fetch`.

```ts title="src/app.ts"
import { registerProvider } from '@flue/runtime';
import { flue } from '@flue/runtime/routing';
import { Hono } from 'hono';

registerProvider('ollama', {
  api: 'openai-completions',
  baseUrl: 'http://localhost:11434/v1',
});

const app = new Hono();

app.use('*', async (c, next) => {
  const started = Date.now();
  await next();
  console.log(`app: ${c.req.method} ${c.req.path} → ${c.res.status} (${Date.now() - started}ms)`);
});

app.get('/api/ping', (c) => c.json({ pong: true, at: new Date().toISOString() }));

app.route('/', flue());

export default app;
```

<ResponseField name="default export" type="Hono | { fetch(request) }">
Must expose a `fetch` method. On Cloudflare, `fetch(request, env, ctx)` is supported. Invalid exports fail at startup with a clear error.
</ResponseField>

When `app.ts` is absent, the generated entry calls `createDefaultFlueApp()`, which mounts `flue()` at `/` without requiring `hono` in user code.

<Note>
`flue.config.ts` holds build knobs (`target`, `root`, `output`). Provider and model registration belong in `app.ts` because API keys and bindings are runtime concerns.
</Note>

The same `app.ts` shape works on Node and Cloudflare. For Cloudflare-specific provider overrides (for example AI Gateway), register providers in `app.ts` — user imports run before auto-registration.

## Generated server entry

`flue build` does not execute user modules directly. It:

1. Discovers modules under `sourceRoot`.
2. Generates an entry that imports each agent, workflow, and channel.
3. Normalizes exports via `normalizeBuiltModules(...)`.
4. Configures `@flue/runtime` with handlers, middleware, persistence, and sandboxes.
5. Composes or defaults the HTTP app.
6. Writes `dist/server.mjs` (Node) or Cloudflare Vite inputs under `.flue-vite/`.

```mermaid
flowchart TB
  subgraph project["Project root"]
    config["flue.config.*"]
    source["sourceRoot<br/>.flue/ or src/ or root"]
  end

  subgraph discovered["CLI discovery"]
    agents["agents/*.ts"]
    workflows["workflows/*.ts"]
    channels["channels/*.ts"]
    app["app.ts optional"]
    db["db.ts optional Node"]
    cf["cloudflare.ts optional CF"]
  end

  subgraph generated["Generated entry"]
    norm["normalizeBuiltModules"]
    runtime["configureFlueRuntime"]
    http["app.fetch or createDefaultFlueApp"]
  end

  config --> source
  source --> discovered
  discovered --> norm
  norm --> runtime
  app --> http
  db --> runtime
  runtime --> http
  http --> artifact["dist/server.mjs or Worker"]
```

## Comparison with Eve layout

In this workspace, **Eve** (`vercel-eve`) uses a single `agent/` slot with path-derived names (`agent/tools/get_weather.ts` → `get_weather`). **Flue** uses plural directories under `sourceRoot` with filename-derived names and separate workflow and channel trees.

| Concern | Flue | Eve |
| --- | --- | --- |
| Authored root | `.flue/`, `src/`, or project root | `agent/` |
| Agents | `agents/<name>.ts` default-export `createAgent` | `agent/agent.ts` + instructions/tools/skills slots |
| Automations | `workflows/<name>.ts` exports `run` | Schedules and session turns |
| Build config | `flue.config.*` at project root | `eve` CLI + compiled `.eve/` |
| HTTP composition | Optional `app.ts` + `flue()` | Stable HTTP session API |

Both frameworks stay provider-neutral: model and tool wiring are project-owned, not locked to a single vendor.

## Scaffold and verify

<Steps>
<Step title="Initialize config">
Run `flue init --target node` (or `cloudflare`) to write `flue.config.ts` with `defineConfig` from `@flue/cli/config`.
</Step>
<Step title="Add modules under source root">
Create `src/agents/` and `src/workflows/` (or `.flue/…` if using that layout). Add at least one agent or workflow file.
</Step>
<Step title="Optional app.ts">
Add `src/app.ts` when you need custom routes, middleware, or `registerProvider` calls.
</Step>
<Step title="Build and inspect discovery">
Run `flue build --target node`. Build output lists `source`, `agents`, `workflows`, and `channels` names discovered from `sourceRoot`.
</Step>
</Steps>

<RequestExample>
```bash
flue build --target node
```
</RequestExample>

<ResponseExample>
```text
flue build
  target   node
  output   dist
  source   src
  app      src/app.ts

agents
  session-test

workflows
  hello
  with-subagent
  ...
```
</ResponseExample>

<Tip>
Run `flue dev` to start the dev server with the same discovery rules. Edits under `agents/`, `workflows/`, `channels/`, `app.ts`, or `cloudflare.ts` trigger rebuilds.
</Tip>

## Related pages

<CardGroup>
<Card title="Flue quickstart" href="/flue-quickstart">
Scaffold a project, run `flue dev`, invoke workflows, and connect to agents.
</Card>
<Card title="Configuration reference" href="/flue-configuration-reference">
`flue.config.*` keys, source-root precedence, env loading, and CLI overrides.
</Card>
<Card title="Build Flue agents" href="/build-flue-agents">
`createAgent`, profiles, tools, skills, sandboxes, and route handlers.
</Card>
<Card title="Flue workflows" href="/flue-workflows">
Define `run()`, initialize agents, and inspect runs with `flue logs`.
</Card>
<Card title="Eve project layout" href="/eve-project-layout">
Contrast Eve's `agent/` slot and path-derived naming with Flue's module directories.
</Card>
</CardGroup>
