Headless agents with the Lite SDK
The 10-line path to a working headless agent — defineAgent + stateless runtime + the Lite SDK entrypoint.
Fabric Harness separates three choices that are easy to mix up:
- SDK entrypoint —
@fabric-harness/sdk/litefor a small import surface, or@fabric-harness/sdkfor the full production surface. - Runtime —
stateless,inline, ortemporal. - Target — where the agent runs, such as
node,temporal-worker,docker, orcloudflare.
This page shows the smallest headless setup: defineAgent + runtime: 'stateless' + @fabric-harness/sdk/lite. The same init(), session.prompt(), session.shell(), session.skill(), and session.task() APIs work from the Full SDK entrypoint too.
The 10-line agent
import { defineAgent } from '@fabric-harness/sdk/lite';
export default defineAgent(async ({ init, payload }) => {
const agent = await init({ runtime: 'stateless' });
const session = await agent.session();
const message = (payload as { message?: string })?.message ?? 'Say hi.';
return { reply: await session.prompt<string>(message) };
}, { name: 'echo', triggers: { webhook: true } });Run it:
export FABRIC_MODEL=anthropic/claude-sonnet-4-6
export ANTHROPIC_API_KEY=...
fabric-harness run echo --message "hello"Or expose it as a webhook:
fabric-harness dev
curl -X POST http://localhost:8787/agents/echo \
-H "Content-Type: application/json" \
-d '{"message":"hello"}'That's it. No schemas to define, no policy to author, no session store to configure.
What the pieces do
defineAgent
defineAgent(handler, options?) is a thin shorthand for agent({ run: handler, ...options }) with no input/output schema. The payload is unknown, the return value passes through unchanged. Use it when typed validation is more ceremony than value.
runtime: 'stateless'
Disables session persistence. No store writes, no artifact disk persistence, no approval waiting. Each invocation is independent. This is the right choice for:
- High-volume webhook handlers where state would just be discarded.
- Edge runtimes (Cloudflare Workers, Vercel Edge) with no durable storage attached.
- Quick prototypes where conversation continuity isn't yet a concern.
In production (FABRIC_ENV=production or NODE_ENV=production), choosing inline without an explicit SessionStore will emit a warning unless you set FABRIC_ALLOW_EPHEMERAL_STATE=1 or pick runtime: 'stateless' explicitly. This prevents surprise data loss.
@fabric-harness/sdk/lite
A trimmed entry point that re-exports the headless surface — init, defineAgent, agent, schema, defineCommand, sandbox helpers, MCP — without pulling Temporal, durable stores, or telemetry exporters into the import graph. Smaller bundles for edge deployments.
For the full surface, import from @fabric-harness/sdk directly. Both entry points share the same runtime; nothing diverges.
The Lite SDK is not feature-light
The Lite SDK skips import weight and ceremony, not the shared agent primitives. Skills, roles, scoped commands, MCP tools, and tasks all work — they just do not require typed input/output metadata or a policy authoring step.
Skills (Markdown-defined procedures)
Drop a Markdown file under .fabricharness/skills/ and call it from your agent. Skills are discovered automatically by the workspace loader.
---
name: triager
description: Search the knowledge base for the best matching article and write a concise, friendly reply.
---
You are a customer support triager.
Steps:
1. Use grep / glob / read over `/workspace/kb` to find articles relevant to the customer's question.
2. Quote the most directly relevant lines.
3. Write a 2–4 sentence reply in the brand's voice (friendly, concise, no jargon).
4. If nothing matches, say so honestly and suggest the customer contact support@example.com.import { defineAgent } from '@fabric-harness/sdk/lite';
export default defineAgent(async ({ init, payload }) => {
const session = await (await init({ runtime: 'stateless' })).session();
const message = String((payload as any)?.message ?? '');
const reply = await session.skill<string>('triager', { args: { message } });
return { reply };
}, { name: 'support', triggers: { webhook: true } });session.skill('triager', { args }) injects the skill body as the prompt, templates the args in, and returns the model's response. The same primitive exists in Lite and Full SDK agents. Full metadata agents can also declare default inline skills with agent({ skills: [...] }) and can add result: schema for typed validation.
Roles (system-prompt overlays)
Roles let you change the agent's voice or expertise without rewriting the prompt every time. Drop a Markdown file under .fabricharness/roles/:
---
name: concise
description: Reply in 1-3 sentences. No bullet points unless explicitly asked. Plain prose only.
---
You write extremely concise replies. Aim for 1–3 sentences. Use plain prose,
not bullet points or headings, unless the user asked for structured output.
Never repeat the question back. Never apologize for the length of the answer.Use it on a session, prompt, or skill call:
const session = await agent.session('thread-1', { role: 'concise' });
await session.prompt('What does this code do?'); // uses 'concise'
await session.skill('triager', { args, role: 'concise' }); // override on the callPrecedence: per-call role > session role > agent role.
Scoped commands (privileged CLIs)
defineCommand binds a CLI like gh or npm with its env (and therefore its secrets) at the command level — secrets never enter model context. Grant the command per-call:
import { defineAgent, defineCommand } from '@fabric-harness/sdk/lite';
const gh = defineCommand('gh', { env: { GH_TOKEN: process.env.GH_TOKEN } });
const npm = defineCommand('npm');
export default defineAgent(async ({ init, payload }) => {
const session = await (await init({ sandbox: 'local', runtime: 'stateless' })).session();
const result = await session.skill<string>('triage-issue', {
args: { issueNumber: (payload as any)?.issueNumber },
commands: [gh, npm], // available only inside this skill call
});
return { result };
}, { name: 'triage' });The agent can run gh issue view 42 ... because gh is in commands. It cannot read GH_TOKEN directly — the token is bound to the gh invocation by the runtime, not exposed to the model.
Tasks (delegated subagents)
session.task(prompt, options?) runs a child session with its own message history but the same sandbox/filesystem — perfect for parallel research or focused subgoals.
const research = await session.task('Read /workspace/kb/billing/* and summarize the refund policy in 5 bullets.');
const reply = await session.prompt(`Use this research to answer the customer:\n\n${research}`);Tasks support a role and cwd override, so you can run them as a "researcher" without changing the parent role.
Mounting a knowledge base
Combine withFilesystemSources with the Lite SDK for a compact support agent in ~25 lines:
import { defineAgent, localDirectorySource, withFilesystemSources } from '@fabric-harness/sdk/lite';
const sandbox = withFilesystemSources('empty', [{
mountAt: '/workspace/kb',
source: localDirectorySource('./knowledge-base', { include: (p) => p.endsWith('.md') }),
}]);
export default defineAgent(async ({ init, payload }) => {
const session = await (await init({ sandbox, runtime: 'stateless' })).session();
const reply = await session.skill<string>('triager', {
args: { message: String((payload as any)?.message ?? '') },
});
return { reply };
}, { name: 'support', triggers: { webhook: true } });See Filesystem sources for more.
MCP tools
connectMcpServer works the same from the Lite SDK — just don't forget to close() the connection in a finally:
import { connectMcpServer, defineAgent } from '@fabric-harness/sdk/lite';
export default defineAgent(async ({ init, payload, env }) => {
const github = await connectMcpServer('github', {
url: 'https://mcp.github.com/mcp',
headers: { Authorization: `Bearer ${env?.GITHUB_TOKEN}` },
});
try {
const session = await (await init({ tools: github.tools, runtime: 'stateless' })).session();
return await session.prompt(String((payload as any)?.prompt ?? ''));
} finally {
await github.close();
}
}, { name: 'gh-assist' });Progressive disclosure: when to graduate
The Lite SDK can be a starting point. Add production features one by one as the work demands them:
| You want to... | Switch to... | What changes in the agent |
|---|---|---|
| Validate inputs / outputs | agent({ input, output, run }) | Add Fabric schemas; defineAgent doesn't validate. |
| Restrict shell / tools | policy: CapabilityPolicy on init or session.prompt | Allow/deny/requireApproval lists. |
| Bind a privileged CLI | defineCommand('gh', { env: { GH_TOKEN } }) + commands: [gh] | Secrets bound at the command, never exposed to the model. |
| Run a Markdown skill | session.skill('triage', { args, result }) | Drop the skill into .fabricharness/skills/triage.md. |
| Persist conversations | runtime: 'inline' + a SessionStore (SQLite/Postgres/file from @fabric-harness/node) | Same init(), just pass store. |
| Write durable artifacts | session.artifact(name, content) | Requires a store that implements putArtifact. |
| Survive crashes / restarts | runtime: 'temporal' + @fabric-harness/temporal | The agent code is unchanged; the runtime swaps. |
Lite vs Full SDK at a glance
import { defineAgent } from '@fabric-harness/sdk/lite';
export default defineAgent(async ({ init, payload }) => {
const session = await (await init({ runtime: 'stateless' })).session();
return { reply: await session.prompt<string>(String((payload as any)?.message)) };
}, { name: 'echo' });import { agent, schema } from '@fabric-harness/sdk';
import type { CapabilityPolicy } from '@fabric-harness/sdk';
const policy: CapabilityPolicy = {
commandPolicy: {
allow: ['git status*', 'git diff*'],
deny: ['git push*'],
requireApproval: ['gh issue comment**'],
},
maxCommandTimeoutMs: 30_000,
};
const inputSchema = schema.object({ issueNumber: schema.number(), title: schema.string() });
const outputSchema = schema.object({ severity: schema.enum(['low','medium','high']), summary: schema.string() });
export default agent({
name: 'triage',
input: inputSchema,
output: outputSchema,
model: process.env.FABRIC_MODEL,
skills: [{ name: 'triage', content: 'Triage the issue and return a concise structured result.' }],
run: async ({ init, input }) => {
const session = await (await init({ policy })).session();
const result = await session.skill('triage', { args: input, policy, result: outputSchema });
await session.artifact('triage.md', `# ${input.title}`, { contentType: 'text/markdown' });
return result;
},
});The CLI, dev server, and deploy targets are shared. The SDK entrypoint and runtime choice are independent; see SDK entrypoints, runtimes, and targets.
Anti-patterns
- Don't fork your code into Lite and Full copies. Graduate features in place when you need typed I/O, policy, durable stores, telemetry, or provider classes.
- Don't use
runtime: 'stateless'when you need approvals. The approval flow needs a store to wait on; in stateless mode it falls through immediately. - Don't import from both
@fabric-harness/sdkand@fabric-harness/sdk/litein the same agent. Pick one entrypoint. The Full SDK is the broader surface; the Lite SDK is the smaller import graph.
See also
- examples/minimal — the 10-line example end-to-end.
- examples/support-agent — Lite SDK + filesystem-mounted knowledge base.
- SDK entrypoints, runtimes, and targets — how the choices fit together.
- Runtime modes —
inlinevsstatelessvstemporal. - Filesystem sources — mount read-only content into the sandbox.