This guide takes you from an installed CLI to a live local agent service.
Prerequisites
- Node >= 22.19 (
node --version). - FastAgent CLI:
npm i -g @fastagent-sh/fastagent.
Model credentials come in step 2 — after the agent exists, because fastagent login stores them
per project.
List available model specs with:
fastagent models1. Create an agent
fastagent init my-agent
cd my-agentThe default scaffold is a self-iterating agent — it is files, and it can edit its own definition (persona.md and skills are re-read every turn). The agent lands in fastagent/; the directory around it is its WORKSPACE — what it works on, and where its AGENTS.md is read from:
my-agent/ # the workspace — the agent's cwd, untouched by init
└── fastagent/ # the agent
├── persona.md # its identity — how to improve yourself
├── skills/writing-great-skills/ # the example skill: how to author skills well
├── tools/fetch-url.ts # an example code tool
├── fastagent.config.mjs
├── package.json
├── .secrets/.env.example # secrets live here, never committed
└── .gitignorepersona.md teaches the agent to capture durable improvements as new skills; writing-great-skills (vendored from mattpocock/skills) is the guide it consults to write them. No AGENTS.md is scaffolded — that file is project context the agent reads (yours, or a host repo’s), not its identity. Add more skills with fastagent add skill <owner/repo/path>. For a agent with no code tool or dependencies (persona.md + the skill + config only):
fastagent init my-agent --minimal2. Inspect it
fastagent infoinfo is read-only. It prints the model, persona, context files (AGENTS.md), skills, discovered tools, channels, diagnostics, and session path without starting a server.
Initializing inside an existing project? Same command, same result: init puts the WHOLE agent into ./fastagent/ — zero writes elsewhere, so the project’s build and the agent’s surface never sweep each other, and the repo’s own AGENTS.md is read as project context. A fastagent.config.* file identifies the agent; fastagent/ is only the default directory name.
The repository IS the agent? (A standalone agent repo, or a monorepo package.) fastagent init . --flat puts the same shape at the root instead. Existing files are kept untouched, and the agent’s workspace is its own directory, so its tools operate on its own definition. Want a different directory name? fastagent init . --agent-dir bot — the fastagent.config.* inside is what makes a directory an agent, never its name.
A fresh agent presets no model. On the first fastagent dev (or start / invoke) in a
terminal, FastAgent shows the full model catalog — models whose provider already has credentials (a
stored login, or a provider API key in your env/.env) come first, annotated with the source; picking
one that needs auth runs the login flow right there — and writes your pick back to
fastagent.config.mjs. Credentials are stored per project (<agent dir>/.secrets/auth.json, no global
fallback), so a login from another directory is invisible here. To set the model non-interactively
(or in CI/deploy, where there is no prompt):
fastagent dev --model provider/model-id
FASTAGENT_MODEL=provider/model-id fastagent dev
# or edit fastagent.config.mjs3. Run locally
fastagent devdev assembles the agent and serves it on :8787. persona.md/AGENTS.md/skills/ edits go live on the next turn; code edits (tools/, channels/, config) restart the worker. The default channel is POST /invoke.
Send one turn:
curl -N -X POST localhost:8787/invoke \
-H 'content-type: application/json' \
-d '{"session":"s1","text":"Summarize https://example.com in two bullets"}'The response is Server-Sent Events. Events include text, optional thinking, tool events, and exactly one terminal completed or failed.
data: {"type":"tool_started","id":"tool-1","name":"fetch-url","args":{"url":"https://example.com"}}
data: {"type":"tool_ended","id":"tool-1","isError":false,"content":{"details":{"url":"https://example.com/","text":"Example Domain …"}}}
data: {"type":"completed"}Reuse the same session value to continue a conversation. Local sessions persist under <state root>/sessions (default fastagent/.state/sessions), so a dev restart keeps conversation history.
4. Try authoring loops
Open the same assembled agent in pi’s interactive TUI:
fastagent chatRun one agent turn without a server:
fastagent invoke "Summarize persona.md in one sentence"Run one tool without a model:
fastagent tool fetch-url '{"url":"https://example.com"}'5. Add a tool
Tools are files in tools/. The filename is the tool name.
// tools/reverse.ts
import { defineTool, z } from "@fastagent-sh/fastagent";
export default defineTool({
description: "Reverse a string.",
input: z.object({ text: z.string() }),
async execute({ text }) {
return { reversed: [...text].reverse().join("") };
},
});Test it directly:
fastagent tool reverse '{"text":"hello"}'Mention the tool in persona.md so the model knows when to use it. fastagent dev reloads on save.
6. Serve without watch
fastagent startstart uses the same assembly as dev, but does not watch files. There is no build step: copy the agent to a host with Node >= 22.19, install dependencies, and run fastagent start.
For deployments, point both machinery roots at durable storage: the state root (sessions and channel state — Telegram’s durable turn replay lives there too) and the secrets dir (the agent’s .env and the auth.json an OAuth refresh rotates on the box):
FASTAGENT_STATE_DIR=/data/.state FASTAGENT_SECRETS_DIR=/data/.secrets fastagent start(FASTAGENT_SESSIONS_DIR / --sessions-dir override just the sessions path; they do not move channel state, and neither knob moves auth.json.)
7. Add channels
Add a first-party channel:
fastagent add github
fastagent add telegram
fastagent add slack
fastagent add feishu # 飞书 (open.feishu.cn)
fastagent add lark # Lark internationalThen run locally with a public tunnel for webhook testing:
fastagent dev --tunnelRead Channels for the channel model, GitHub channel for GitHub webhooks, Telegram channel for Telegram bots, Slack channel for Slack apps, and Feishu channel (Lark compatibility) for Feishu and Lark bots.
8. Run on a clock
Channels turn external events into invocations; schedules do the same for the clock — firing the
agent on a cron: a daily digest, a periodic check. Drop a file in schedules/ (mirroring tools/), named by its filename:
// schedules/daily-digest.ts
import { defineSchedule } from "@fastagent-sh/fastagent";
export default defineSchedule({
cron: "0 9 * * *",
tz: "America/New_York",
prompt: "Summarize yesterday's activity and send it to the team Telegram.",
});The prompt must say where output goes — the scheduler only fires the agent; delivery is a send tool’s
job. fastagent add telegram scaffolds one (tools/telegram-send.ts sends a message or a file); and
because a scheduled turn runs outside any chat, the agent has no chat context — put the target chat
id in the prompt (“…send it to Telegram chat -100123456”). Test it immediately (without waiting for
the cron, and without touching the real fire state):
fastagent fire daily-digestOn resident hosts, the cron fires while dev/start is serving; keep the process running.
AgentCore ingress instead uses EventBridge and supports scale-to-zero.
fastagent schedule history <name> answers “did last night’s run silently fail?”, and
fastagent schedule list shows the selected local state’s pending work. Agents can
also schedule themselves (a built-in wake tool — “check the deploy in 10 minutes”) — opt in with
selfSchedule: true in fastagent.config.*. See the CLI reference and
API reference.
Where next
- Agent development guide — responsibilities, TypeScript, verification, and host-specific operation.
- Embedding — use FastAgent as a library inside your own app.
- Channels — webhook and bot adapters.
- Deploy — ship the directory to Fly, Railway, AWS Bedrock AgentCore, or any Docker host.
- Agent Handler SPEC — the event stream contract.
- Core design — maintainer architecture notes.

