# Flue: Headless, Programmable AI Agent Framework from the Astro Team

[Flue](https://github.com/floatplane/flue) is an **open-source TypeScript framework for building AI agents**, developed by the Astro team. It was originally built to automate AI workflows inside Astro's own GitHub repositories. Its design is headless and programmable: **agents can run without a human present**, triggered by API calls, webhooks, or cron jobs, and deployable to Node.js or Cloudflare Workers.

<iframe width="100%" height="315" src="https://www.youtube.com/embed/n5cYS6KuyK8" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>


## The harness concept

Flue's documentation defines an AI agent as an LLM running inside a harness. The LLM provides reasoning capability; the harness provides the tools, context, memory, and environment the LLM needs to interact with external systems and complete tasks.

Without a harness, an LLM responds to individual API calls with no persistent state and no tool access. Flue is the programmable harness layer: it provides session management, tool and skill execution, sandbox environments, and a structured output format.

![Documentation page for "What is an agent?" illustrating the concept of an LLM running inside a harness](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/3c2b1f80-73ad-4c8b-10da-3b7c0859a000/lg1x =1920x1080)

## Installation and setup

```command
mkdir flue-tutorial && cd flue-tutorial
```

```command
npm install @flue/runtime
```

```command
npm install --save-dev @flue/cli
```

Create a `.env` file with your LLM provider API key:

```text
[label .env]
ANTHROPIC_API_KEY="your-anthropic-api-key-here"
```

Initialize the project configuration:

```command
npx flue init --target node
```

This creates `flue.config.ts`:

```typescript
[label flue.config.ts]
import { defineConfig } from '@flue/cli/config';

export default defineConfig({
  target: 'node',
});
```

`target` can be `'node'` (Node.js server using Hono) or `'cloudflare'` (Cloudflare Worker with Durable Objects for persistence).

![Flue documentation showing the installation and initialization commands](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/2b269f17-e5c5-46e4-6bb1-29a5cdbb4600/md1x =1920x1080)

## Creating an agent

Flue looks for agent definitions in an `agents/` directory. The filename becomes the agent's ID.

```command
mkdir agents
```

```typescript
[label agents/hello-world.ts]
import { createAgent } from '@flue/runtime';

export default createAgent(() => ({
  model: 'anthropic/claude-3.5-sonnet',
  instructions: 'Tell a funny "hello world" engineering joke.',
}));
```

Connect to the agent interactively:

```command
npx flue connect hello-world local-session
```

`local-session` is the instance ID. It identifies this conversation and enables session persistence across interactions.

After the agent responds, Flue prints a JSON summary:

![Final JSON output in the terminal with response text, token usage, cost, and model ID](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/a12d775e-f7fd-4b42-d1ad-dc1f35fb2f00/lg1x =1920x1080)

The output includes `text` (full response), `usage` (input and output token counts), `cost` (estimated API cost broken down by input, output, and cache), and `model` (the model ID used).

## Building a workflow with a skill

Workflows perform a finite unit of work from input to result. They live in a `workflows/` directory and export a `run` function.

A skill is a Markdown file containing instructions for the LLM on how to perform a specific reusable task. Flue imports it as structured context.

```typescript
[label workflows/yt-titles.ts]
import { createAgent, type FlueContext } from '@flue/runtime';
import { readFile } from 'node:fs/promises';
import titleScore from '../skills/title-score/SKILL.md' with { type: 'skill' };

const agent = createAgent(() => ({
  model: 'anthropic/claude-3.5-sonnet',
  instructions:
    'Study the provided script and generate 10 clickbait YouTube titles. Rank these titles from best to worst using the title-score skill. Just give me the titles and scores in a table and nothing else.',
  skills: [titleScore],
}));

export async function run(init: any, payload: FlueContext<{ path: string }>) {
  const harness = await init(agent);
  const session = await harness.session();
  const script = await readFile(payload.path, 'utf8');
  const response = await session.prompt(script);
  return { summary: response.text };
}
```

![Code for the YouTube titles workflow showing the imported skill and the run function structure](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/696ef12a-d2f5-45a8-34bb-5207fcf73e00/md1x =1920x1080)

## Sandboxes

The default sandbox is `just-bash`: an in-memory TypeScript reimplementation of a Bash shell. It requires no Docker container or virtual machine, making it fast and cheap for agents that do not need filesystem access.

For workflows that need to read local files or run external scripts, the `local` sandbox provides direct filesystem access:

```typescript
[label workflows/yt-titles.ts]
import { local } from '@flue/runtime/node';

const agent = createAgent(() => ({
  model: 'anthropic/claude-3.5-sonnet',
  instructions: '...',
  skills: [titleScore],
  sandbox: local(),
  cwd: '/path/to/project/skills/title-score',
}));
```

`sandbox: local()` replaces the in-memory sandbox with the local machine's filesystem. This is less isolated but necessary when the agent needs to read files or execute local scripts.

![Content of a SKILL.md file showing how instructions and bash commands are defined for the agent](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/a9d03c31-a2aa-4b4d-0ab9-3a429dd12b00/md2x =1920x1080)

## Exposing a workflow as an HTTP endpoint

Add a route handler to the workflow file:

```typescript
[label workflows/yt-titles.ts]
import type { WorkflowRouteHandler } from '@flue/runtime/node';

export const route: WorkflowRouteHandler = async (_c, next) => {
  // add authentication or validation here
  return next();
};
```

Build the server:

```command
npx flue build --target node
```

This produces `dist/server.mjs`.

```command
PORT=8080 node dist/server.mjs
```

Trigger the workflow:

```command
curl -X POST http://localhost:8080/workflows/yt-titles \
  -H "Content-Type: application/json" \
  -d '{"path": "/path/to/your/script.md"}'
```

The server responds immediately with a `runId`:

```json
{"status":"accepted","runId":"workflow:yt-titles:01KT..."}
```

Poll for the result:

```command
curl http://localhost:8080/runs/workflow:yt-titles:01KT...
```

![JSON response from a GET request showing the completed workflow status and the summary result with ranked titles](https://imagedelivery.net/xZXo0QFi-1_4Zimer-T0XQ/065463b6-b6bb-4720-1637-2218519dde00/md2x =1920x1080)

## Final thoughts

**Flue's sandbox design is its most practically useful feature for cost management**. The default `just-bash` in-memory sandbox means most agents incur no infrastructure cost beyond the API call itself. The `local()` sandbox is an explicit opt-in for cases that require it, keeping the default path cheap and fast.

The `createAgent` + `SKILL.md` + `run` function pattern is low-ceremony. **Most of the agent's behavior is defined in Markdown rather than code**, which makes it easy to adjust instructions without restructuring the application.

For teams using Astro who want headless AI agent capabilities without building their own orchestration layer, Flue is a natural fit. For teams on other stacks, the same principles apply; the main consideration is whether deploying to Node.js or Cloudflare Workers fits the existing infrastructure.

Documentation and source code are at [github.com/floatplane/flue](https://github.com/floatplane/flue).