Cookbook
Getting Started
How do I install Agent Apps?
git clone --depth=1 --branch release ssh://git.amazon.com/pkg/AgentApps.git ~/.agent-apps/cli
bash ~/.agent-apps/cli/scripts/install.sh
This installs the agent-apps command globally. For markdown-only projects, you can create skill files directly without npm install — the global CLI runs them.
How do I add Agent Apps as a project dependency?
Only needed if your code skills import from agent-apps (most don't — ctx is passed as an argument):
npm install git+ssh://git.amazon.com/pkg/AgentApps.git#release
How do I create a project?
agent-apps init
Creates main.skill.md, a hello skill, README, .gitignore, and package.json with "type": "module". Also installs the runtime as a local dependency.
How do I run a skill?
agent-apps hello --name World # by name (flags become args)
agent-apps hello --arg name=World # explicit --arg form
agent-apps ./skills/hello.skill.js # by path
agent-apps # run the main skill (main.skill.md)
How do I update the CLI?
agent-apps update
How do I use the REPL?
agent-apps repl
agent-apps repl --task "Summarize my tasks" # start with an initial task
Interactive agent session with streaming output, history, and tab completion. Ctrl-C cancels the current turn.
How do I inspect available skills?
agent-apps skill-list # list public skills
agent-apps skill-list --query files # search by name/description
agent-apps skill-list --tags '["api"]' # filter by tags (AND logic)
agent-apps skill-describe --ref hello # full details on a skill
agent-apps help --ref shell # GNU-style usage docs
Writing Skills
How do I write a code skill?
// skills/add.skill.js
export const frontmatter = {
name: 'add',
description: 'Add two numbers',
metadata: {
params: {
type: 'object',
properties: { a: { type: 'number' }, b: { type: 'number' } },
required: ['a', 'b']
}
}
};
export default async function(ctx, { a, b }) {
return { sum: a + b };
}
ctx is passed as an argument — no imports needed. Return a value to set the result.
What are the context object properties?
ctx.args — arguments passed to your skill (read-only)
ctx.locals.result — current result (set by returning a value)
ctx.locals.config — project config (cwd, workspace, paths, model)
ctx.globals — long-lived state shared across invocations
ctx.locals — per-invocation scratchpad
ctx.nonlocals — inherited parent→child (gateway, agent memory)
ctx.run.origin — tool source (uri, frontmatter)
ctx.run.middleware — fully resolved cascade metadata for this invocation
ctx.locals.prompt — prompt content { raw, expanded }
ctx.run.tool — metadata (name, description, params, returns, role, tags, visibility, allowedTools)
ctx.envelope.target — self for normal skills, served context for middleware
ctx.run.signal — AbortSignal (cancelled when parent aborts)
ctx.run.interactive — true when running in a direct CLI session with a real TTY
What are the ctx.manager methods?
await ctx.manager.invoke('skill-name', args) // call another skill
ctx.manager.finish(value) // set result, stop pipeline
ctx.manager.fail(error) // throw and unwind
await ctx.manager.next() // advance middleware chain (middleware only)
ctx.manager.get('config.cwd') // read by dotted path
ctx.manager.set('locals.flag', true) // write by dotted path
ctx.manager.abort('reason') // cancel this + children
await ctx.manager.inject([{ name: 'mw', after: ['$configure'] }]) // insert middleware
await ctx.manager.refresh() // re-resolve tool against updated paths
ctx.manager.log('message') // emit a log event (string or object)
ctx.manager.tail({ tag: 'agent', last: 10 }) // query recent log events
await ctx.manager.configure({ level: 'debug' }) // configure log settings
What directives can I use in markdown skills?
| Directive | Replaced with |
|---|---|
:arg[name] |
Value of argument name |
:env[VAR] |
Environment variable |
:path[./rel] |
Absolute path resolved from skill's directory |
:skill[name] / :tool[name] |
Another skill's description |
:eval[expr] |
Result of a JavaScript expression (ctx available as ctx) |
:context[a.b.c] |
Value at dotted path on ctx |
:config[a.b] |
Value from ctx.locals.config |
:script[lang]{src=file} |
File contents as fenced code block |
:inline[path] |
File contents inline (no fences) |
HTML comments (<!-- ... -->) are stripped from prompts before processing.
How do I write a markdown skill?
---
name: summarize
description: Summarize text
allowed-tools: file-read
metadata:
params:
type: object
properties:
text: { type: string }
required: [text]
---
Summarize the following text in 2-3 sentences:
:arg[text]
Requires AWS credentials with Bedrock access. The agent interprets the prompt and uses the tools listed in allowed-tools.
How do I delegate to code from markdown?
metadata:
delegate: my-handler # bare name — calls another skill
delegate: ./lib/handler.js # file path
delegate: "inline://code,export default async function(ctx, { name }) { return `Hello, \\${name}!`; }"
delegate: "inline://markdown,---\nname: x\n---\nDo the thing."
delegate: "inline://base64,<base64-encoded inline:// ref>"
The markdown body stays as documentation. The delegate runs instead of the agent. The inline:// scheme format is inline://<tag>,<payload> where tag is code, markdown, or base64. Escape \${ in YAML to prevent metadata interpolation. Use base64 when the payload fights with YAML quoting.
How do I use structured returns?
metadata:
params:
type: object
properties:
text: { type: string }
returns:
type: object
properties:
summary: { type: string }
wordCount: { type: number }
required: [summary]
The agent must call ctx.manager.finish(value) with data matching the schema. Mismatches trigger a retry.
How do I include shared context in prompts?
metadata:
include: style-guide # prepend file content to prompt
include: # multiple includes
- company-rules
- data-schema
Included content is prepended before directive expansion. The default pipeline includes agent-context (channel description) automatically.
How do I write a folder skill?
skills/notes/
SKILL.md ← frontmatter + prompt
The directory name becomes the tool name (notes). Use metadata.delegate to point to a code handler in the same directory or elsewhere.
How do I make a skill executable?
Add a shebang line:
#!/usr/bin/env agent-apps
chmod +x ./skills/hello.skill.md
./skills/hello.skill.md --name World
Extending the Framework
How do I write custom middleware?
// skills/timer.skill.js
import { ctxTarget } from 'agent-apps';
export const frontmatter = {
name: 'timer',
metadata: { role: 'middleware', visibility: 'hidden', tags: [] }
};
export default async function(ctx) {
const target = ctxTarget(ctx);
const start = Date.now();
await target.manager.next(); // run the rest of the served pipeline
console.log(`${target.run.tool.name}: ${Date.now() - start}ms`);
}
Activate it: metadata: { $order: { timer: { before: [execute] } }, timer: }
How do I create a custom metadata handler?
Create a skill whose name matches the metadata key. metadata.rate-limit triggers skill rate-limit:
// skills/rate-limit.skill.js
import { ctxTarget } from 'agent-apps';
export const frontmatter = {
name: 'rate-limit',
metadata: { role: 'middleware', visibility: 'hidden', tags: [] }
};
export default async function(ctx, args) {
const target = ctxTarget(ctx);
target.locals.rateLimit = args.requests ?? 100;
await target.manager.next();
}
Any skill can now use it: metadata: { rate-limit: { requests: 50 } }
How do I create a custom directive?
Create a skill whose name matches the directive. :uppercase[text] triggers skill uppercase:
// skills/uppercase.skill.js
import { ctxTarget } from 'agent-apps';
export const frontmatter = { name: 'uppercase' };
export default async function(ctx, args) {
const target = ctxTarget(ctx);
if (target.locals.prompt) {
target.locals.prompt.expanded = target.locals.prompt.expanded
.replace(new RegExp(`:uppercase\\[${args.label}\\](\\{[^}]*\\})?`, 'g'), args.label.toUpperCase());
}
await target.manager.next();
}
How do I override a built-in skill?
Place a skill with the same name earlier in the search path. Your ./skills/file-read.skill.js shadows the library's file-read:
export const frontmatter = { name: 'file-read', metadata: { params: { type: 'object', properties: { path: { type: 'string' } }, required: ['path'] } } };
export default async function(ctx, { path }) {
console.log(`Reading: ${path}`);
const { readFile } = await import('node:fs/promises');
return readFile(path, 'utf-8');
}
How do I control which metadata keys are tool invocations?
metadata:
$dynamic: [params, model] # ONLY these are tool invocations, everything else is data
author: someone # data
params: { type: object } # tool invocation
metadata:
$static: [author, version] # these are data, everything else is a tool invocation
How the Pipeline Works
Every ctx.manager.invoke() creates an isolated context and runs a full middleware pipeline. The cascade resolves the tool's metadata, each metadata key matching a tool becomes a pipeline entry, and the entries execute as a Koa-style onion:
$pre-configure
→ cwd → workspace → mcp → gateway → event
→ paths → params → delegate → model → include → roles → ...
$configure → $post-configure
→ directives
$pre-execute
→ trust → validate-args → validate-allowed-tools
$execute
→ execute (runs the tool's fn)
→ validate-returns
→ agent-execute (invokes the LLM for markdown skills)
$post-execute
Each entry calls await target.manager.next() to pass control to the next entry (where target = ctxTarget(ctx)). After the deepest entry returns, control unwinds back through each entry's "after" logic. try/catch/finally works naturally — this is the Koa onion pattern.
Middleware reads from and writes to ctx.envelope.target (the served skill's context). The middleware's own ctx is its private workspace. Phase sentinels ($pre-configure, $configure, $post-configure, $pre-execute, $execute, $post-execute) are ordering anchors — they don't execute.
Debug, profile, and observe are not part of the default pipeline. They are activated by adding their metadata keys to a skill's cascade (e.g., metadata: { debug: true } or metadata: { profile: true }). They use $hook annotations to bind as hooks on the pipeline. See the Events & Hooks specification for details.
Configuration
How do I configure the model?
# main.skill.md
metadata:
role: main
model:
id: us.anthropic.claude-sonnet-4-6
region: us-east-1
temperature: 0.7
maxTurns: 15
Override from CLI: agent-apps --local model.region=us-west-2 hello
How do I control what propagates to child skills?
Server middleware (web, websocket, webhook, email, slack) and per-skill middleware (session, state, auth, roles, schedule, event) automatically keep themselves private via $self — you don't need to declare $private for these. For custom keys, use $private:
metadata:
$private: [my-custom-config] # this key doesn't propagate to children
my-custom-config: { port: 9090 }
model: # public — propagates to all skills
id: us.anthropic.claude-sonnet-4-6
metadata:
$public: [model, paths] # ONLY these propagate, everything else is private
How do I share config across all skills ($global)?
model, trust, and mcp automatically promote themselves to global overrides via $self — you don't need $global for these. For custom keys that need to cross authority boundaries:
metadata:
$global: [my-custom-config] # applies to ALL tools, including hub packages
my-custom-config: { key: value }
$local is the same but scoped to your project (doesn't affect hub packages).
How do I inherit from another skill?
metadata:
$inherit: company-base-config # single parent
$inherit: [base-a, base-b] # multiple (C3 linearization)
$inherit: false # opt out of cascade entirely
How do I control merge behavior?
metadata:
$merge:
paths: prepend # child paths before parent
mcp: shallow # merge objects one level deep
model: shallow
include: append # child includes after parent
Strategies: replace (default), shallow, deep, prepend, append.
How do I remove inherited keys?
metadata:
$remove: [validate-returns] # remove from merged result
How do I control middleware ordering?
metadata:
$order:
my-middleware: { after: [$configure], before: [$post-configure] }
trust: { after: [$pre-execute], before: [$execute] }
Phase sentinels: $pre-configure, $configure, $post-configure, $pre-execute, $execute, $post-execute.
How do I use environment variable overrides?
AGENT_APPS_LOCAL_MODEL__REGION=us-east-1 # → { model: { region: "us-east-1" } }
AGENT_APPS_GLOBAL_MODEL__ID=sonnet # applies to everything
AGENT_APPS_CONFIG_CWD=/other/project # pre-cascade global
Double underscores = nesting. Single underscores become hyphens.
Agent
How do I restrict which tools the agent can use?
allowed-tools: "file-read file-write shell" # explicit list
allowed-tools: "file-* shell" # glob patterns
allowed-tools: "* file-write($deny)" # allow all except file-write
allowed-tools: "* #destructive($deny)" # deny by tag
allowed-tools: "* context-manager(op=finish,$deny)" # deny specific operations
Callback refs (from callback-create or createCallbackRef) are programmatic tools with generated names like callback-a1b2c3d4. They are subject to allowed-tools like any other tool. Allow them with callback-*, or use a custom label for finer control: my-hook-*.
How do I run a subagent from code?
const result = await ctx.manager.invoke('agent', {
prompt: 'Analyze the data in data.json and return key insights.',
allowedTools: 'file-read',
returns: { type: 'object', properties: { insights: { type: 'array' } }, required: ['insights'] }
});
The subagent gets its own LLM session with the specified prompt and tools. nonlocals.gateway propagates automatically — the subagent can reach the same user.
To control which tools a subagent can receive, constrain the agent tool's allowedTools arg in your skill's allowed-tools:
allowed-tools: "file-read agent(allowedTools=file-read)"
Without this constraint, the agent could spawn a subagent with any tool set. The arg constraint ensures the subagent's allowedTools must match the specified pattern.
How do I coordinate between subagents?
Via return values (simplest):
const research = await ctx.manager.invoke('agent', { prompt: 'Research X', allowedTools: 'web-fetch' });
const report = await ctx.manager.invoke('agent', { prompt: `Write report based on: ${research}`, allowedTools: 'file-write' });
Via events (real-time):
await ctx.manager.invoke('event-subscribe', { topic: 'progress' });
// Start subagent with event-emit in its allowed-tools
ctx.manager.invoke('agent', { prompt: 'Do work, emit progress events', allowedTools: 'file-read event-emit' });
// Monitor progress
const events = await ctx.manager.invoke('event-wait', { topic: 'progress', timeout: 30000 });
Via files (persistent):
await ctx.manager.invoke('agent', { prompt: 'Write findings to /tmp/findings.json', allowedTools: 'file-write' });
const findings = await ctx.manager.invoke('file-read', { path: '/tmp/findings.json' });
How do I keep an agent running forever (persistent mode)?
metadata:
model:
persistent: true
The persistent flag in model config prevents the agent from terminating. The hook callback always returns { continue: true } at turn-end. Add contextEdit: true to let the agent manage its own context window.
How do I monitor a subagent in real time?
const result = await ctx.manager.invoke('agent', {
prompt: 'Do the work',
allowedTools: 'file-read file-write',
events: 'my-agent-topic' // agent emits turn-start, tool-call, tool-result, message, etc.
});
Subscribe to my-agent-topic with event-subscribe to observe progress.
How do I write a custom agent provider?
Create a skill that accepts { prompt, config, invokeRef, hookRef, userMessage } and runs an LLM loop:
export const frontmatter = {
name: 'my-provider',
metadata: { tags: ['meta-internal'], visibility: 'hidden' },
};
export default async function(ctx, args) {
const { prompt, config, invokeRef, hookRef, userMessage } = args;
const model = createMyLLMClient(config);
await ctx.manager.invoke(hookRef, { type: 'turn-start', turnNumber: 1 });
let message = userMessage ?? 'Execute the task.';
while (true) {
const response = await model.chat(prompt, message);
await ctx.manager.invoke(hookRef, { type: 'message', text: response.text });
for (const call of response.toolCalls) {
const d = await ctx.manager.invoke(hookRef, { type: 'tool-call', tool: call.name, args: call.input });
if (d?.deny) continue;
const result = await ctx.manager.invoke(invokeRef, { code: call.input.code });
await ctx.manager.invoke(hookRef, { type: 'tool-result', tool: call.name, result });
}
const d = await ctx.manager.invoke(hookRef, { type: 'turn-end', result: response.text });
if (d?.stop) return d.result ?? response.text;
if (d?.continue) { message = d.message ?? 'Continue.'; continue; }
return response.text;
}
}
Configure it: metadata: { model: { agent: 'my-provider' } }. See the Agent specification for the full hook protocol.
Invoke Options
How do I override a skill's config at call time?
await ctx.manager.invoke('my-skill', args, {
context: {
locals: {
middleware: {
frontmatter: { 'allowed-tools': 'file-read file-write' },
model: { temperature: 0.9 }
}
}
}
});
Injected middleware is the highest-specificity cascade level (above own keys, below CLI). Keys in locals.middleware are non-cascading (apply to this invocation only). Keys in nonlocals.middleware cascade to child invocations.
Events
How do I publish and subscribe to events?
// Subscribe — function handler
await ctx.manager.invoke('event-subscribe', { topic: 'updates', ref: 'my-handler-skill' });
// Publish
await ctx.manager.invoke('event-emit', { topic: 'updates', type: 'item-created', data: { id: 42 } });
How do I wait for an event?
// Subscribe first (with replay to catch buffered events)
await ctx.manager.invoke('event-subscribe', { topic: 'updates', replay: 10 });
// Block until event arrives
const events = await ctx.manager.invoke('event-wait', { topic: 'updates', timeout: 30000 });
How do I handle events declaratively?
Events are routed to skills via the event middleware's centralized routes: config on the main skill:
# In main skill metadata
metadata:
event:
routes:
- topic: deploy
ref: on-deploy
- topic: rollback
ref: on-rollback
How do I unsubscribe?
await ctx.manager.invoke('event-unsubscribe', { topic: 'updates', ref: 'my-handler-skill' });
await ctx.manager.invoke('event-unsubscribe', { topic: 'updates' }); // remove all
How do I use manual acknowledgment?
const events = await ctx.manager.invoke('event-wait', { topic: 'jobs', ack: false });
// Process...
await ctx.manager.invoke('event-ack', { topic: 'jobs', ids: events.map(e => e.id) });
// Or reject (returns to front of queue):
await ctx.manager.invoke('event-ack', { topic: 'jobs', ids: ['abc'], nack: true });
Channel
How does gateway-send/gateway-receive work?
await ctx.manager.invoke('gateway-send', { message: 'Hello!' });
const reply = await ctx.manager.invoke('gateway-receive', { prompt: 'What is your name?' });
These dispatch through nonlocals.gateway — CLI by default, Slack/email/web when set by a provider. Nested invocations inherit the channel automatically, so subagents reach the same user.
gateway-receive and route dispatch are independent delivery paths. A skill invoked by route dispatch that also calls gateway-receive may see the same message in both paths. For non-consuming observation of inbound messages, use event-wait on the gateway-message topic instead.
How do I set up email?
metadata:
email:
region: us-east-1
bucket: my-email-bucket # S3 bucket for SES inbound
prefix: incoming/
from: agent@mydomain.com
allowed-senders: [alice@example.com]
poll-interval: 30s
Provider: SES (inbound via S3, outbound via SES API).
How do I route emails to skills?
# In main skill metadata — routes dispatch inbound email to skills
metadata:
email:
region: us-east-1
bucket: my-email-bucket
from: agent@mydomain.com
routes:
- ref: handle-support
from: "*@example.com"
subject: "support*"
How do I send/receive email from code?
await ctx.manager.invoke('email-send', { to: 'user@example.com', subject: 'Hi', body: 'Hello' });
const msg = await ctx.manager.invoke('email-receive', { timeout: 60000 });
Slack
How do I set up Slack?
metadata:
slack:
app-token: xapp-1-... # Socket Mode token
bot-token: xoxb-... # Bot OAuth token
allowed-users: [U01ABC123]
Use env var overrides to avoid hardcoding: AGENT_APPS_LOCAL_SLACK__APP_TOKEN=xapp-...
How do I route Slack messages to skills?
metadata:
slack:
app-token: xapp-1-...
bot-token: xoxb-...
routes:
- ref: handler-skill
type: dm # dm, mention, or *
from: "U01ABC*" # glob pattern
How do I send/receive Slack messages from code?
await ctx.manager.invoke('slack-send', { channel: '#general', message: 'Hello' });
const msg = await ctx.manager.invoke('slack-receive', { timeout: 60000 });
Web
How do I serve HTTP routes?
# main.skill.md
metadata:
web:
port: 8080
routes:
- method: GET
path: /
ref: home
Route skills receive { route, query, body, headers, method, path, sessionId } as args. Code skills return strings (HTML) or objects (JSON). Markdown route skills get a default returns schema injected. Static files in public/ served automatically.
How do I control the HTTP response directly?
await ctx.manager.invoke('web-send', { status: 200, headers: { 'content-type': 'text/event-stream' } });
await ctx.manager.invoke('web-send', { write: 'data: hello\n\n' });
await ctx.manager.invoke('web-send', { end: true });
How do I read the HTTP request?
const body = await ctx.manager.invoke('web-receive', {});
WebSocket
How do I serve WebSocket connections?
metadata:
websocket:
routes:
- socket: chat
listen: true
ref: echo
- socket: prices
connect: "wss://api.example.com/v1"
Each message invokes the referenced skill with { message, socket }. Return value is sent back.
How do I send/receive WebSocket messages from code?
await ctx.manager.invoke('websocket-send', { socket: 'chat', data: 'hello' });
const msg = await ctx.manager.invoke('websocket-receive', { socket: 'prices', timeout: 5000 });
Webhook
How do I receive webhooks?
metadata:
webhook:
routes:
- path: /hooks/github
secret: whsec_... # Standard Webhooks HMAC verification
ref: on-push
Webhooks are fire-and-forget — no reply channel. Use env var overrides for secrets.
Trust
How do I gate tool execution with approval?
metadata:
trust:
rules: "file-write code shell" # prompt before these tools
store: file # persist decisions to .agent-apps/trust.json
User responds: y (once), a (always), n (deny once), v (deny forever).
rules: "file-read($policy=allow) *" # auto-approve reads, prompt for everything else
rules: "*($policy=allow)" # trust everything (YOLO)
How do I manage trust decisions?
agent-apps trust-list
agent-apps trust-grant --ref file-write --level always
agent-apps trust-revoke --ref file-write
Roles
How do I assign permissions by identity?
metadata:
roles:
key: "nonlocals.gateway.id"
rules:
- match: "*@mycompany.com"
name: admins
metadata:
frontmatter: { allowed-tools: "*" }
trust: { rules: "#meta-internal($policy=allow) *" }
- match: "*"
name: everyone
metadata:
frontmatter: { allowed-tools: "file-read file-list" }
agent-apps roles-list # show configured roles
Session
How do I give a skill persistent memory?
metadata:
session: args.sessionId # shorthand — correlate by session ID
metadata:
session:
key: args.userId # correlate by user
capture: [$args, $result] # what to persist ($all, $args, $result, $agent)
maxEntries: 100 # rolling window (default 50)
store: file # memory (default) or file (.agent-apps/sessions/)
History is auto-prepended to markdown skill prompts. Use :session-history[] directive to control placement.
Model
How do I configure the agent model?
metadata:
model:
id: us.anthropic.claude-sonnet-4-6
region: us-east-1
temperature: 0.7
maxTurns: 15 # default 30
maxSteps: 200 # default 30
contextEdit: true # let agent manage its own context window
agent: agent-strands # provider (default), or agent-kiro
Include
How do I prepend shared content to prompts?
metadata:
include: style-guide # single file path (relative to skill's directory)
include: [rules, data-schema] # multiple — appended in order
The included file content is prepended before directive expansion.
Schedule
How do I schedule a skill for later?
metadata:
schedule: # enable the schedule system (file persistence by default)
# One-shot: fire after a delay
agent-apps schedule-create --ref health-check --delay 30m --note "periodic"
# One-shot: fire at a specific time
agent-apps schedule-create --ref report --at 2026-04-01T09:00:00Z
# Recurring: cron expression
agent-apps schedule-create --ref cleanup --cron "0 3 * * *" --note "nightly cleanup"
# Recurring with timezone
agent-apps schedule-create --ref standup --cron "0 9 * * 1-5" --timezone US/Eastern
# Manage schedules
agent-apps schedule-list
agent-apps schedule-cancel --id sch_abc123
Schedules persist to .agent-apps/schedules.json by default (store: file). They survive process restarts — on startup, the schedule middleware reloads persisted entries and rearms their timers. Use store: memory for process-lifetime-only schedules.
How do I declare recurring schedules in config?
metadata:
schedule:
routes:
- ref: cleanup
cron: "0 3 * * *"
note: nightly cleanup
- ref: health-check
delay: 5m
note: startup health check
Declarative routes are created on startup. Cron routes automatically re-schedule after each run. Duplicate routes (same ref + cron + timezone) are skipped.
State
How do I persist key-value data?
await ctx.manager.invoke('state-set', { key: 'user:alice:prefs', value: { theme: 'dark' } });
const prefs = await ctx.manager.invoke('state-get', { key: 'user:alice:prefs' });
Default backend is file — persists to .agent-apps/state/ as JSON files. Use store: 'memory' for process-lifetime state.
// List keys by prefix
const keys = await ctx.manager.invoke('state-list', { prefix: 'user:' });
// Wait for a key (cross-skill coordination)
const result = await ctx.manager.invoke('state-wait', { key: 'job:done', timeout: 30000 });
// Delete
await ctx.manager.invoke('state-delete', { key: 'user:alice:prefs' });
Toolchain
How do I compile a skill to code?
agent-apps skill-compile --ref hello # markdown → code (saved to workspace/)
The compiled skill shadows the source via search path priority. Uses SHA-256 hash to skip unchanged skills.
How do I decompile code to markdown?
agent-apps skill-decompile --ref hello # code → markdown
How do I test a skill?
agent-apps skill-test --ref hello # parse check + metadata + unit tests + prompt assessment
Returns { pass, errors, warnings, suggestions, score }.
How do I clean compiled artifacts?
agent-apps skill-clean # wipe workspace/ and clear caches
How do I analyze a skill's environment?
agent-apps skill-analyze --ref hello # cascade, middleware chain, available tools, siblings
agent-apps skill-analyze --ref hello --trace false --interpret false # static profile only
How do I revise a skill's frontmatter and prose?
agent-apps skill-revise --ref hello # full revision
agent-apps skill-revise --ref hello --frontmatterOnly true # metadata only
How do I derive test constraints from output?
agent-apps skill-baseline --ref hello # run on test inputs, cache constraints
How do I generate a project from a description?
agent-apps skill-architect --description "A REST API for managing bookmarks"
How do I search framework documentation?
agent-apps skill-introspect --query "middleware ordering" # semantic search
agent-apps skill-introspect # list all docs (table of contents)
agent-apps skill-introspect --file reference/specification/events.md # read specific file
agent-apps skill-introspect --query "cascade" --file reference/specification/configuration.md # search within file
MCP
How do I consume external MCP tools?
metadata:
mcp:
fs:
command: npx
args: [-y, "@modelcontextprotocol/server-filesystem", ./data]
remote:
url: https://mcp.example.com/tools # Streamable HTTP transport
Tools registered as fs-read-file, fs-list-directory, etc. (server prefix + sanitized name).
How do I expose my skills as an MCP server?
agent-apps mcp-server # stdio (for Claude Desktop, Cursor)
agent-apps mcp-server --transport http --port 8080 # HTTP
agent-apps mcp-server --allowedTools "my-*" # scope exposed tools
Files
How do I read and write files?
const content = await ctx.manager.invoke('file-read', { path: 'data.json' });
await ctx.manager.invoke('file-write', { path: 'out.json', content: '{}' });
// file-write supports: create (default), append, str_replace, insert
await ctx.manager.invoke('file-write', { path: 'log.txt', content: 'new line', command: 'append' });
How do I list and find files?
const entries = await ctx.manager.invoke('file-list', { path: './data', depth: 2 });
// Returns: [{ name: 'file.txt', type: 'file' }, { name: 'subdir', type: 'directory' }]
const found = await ctx.manager.invoke('file-glob', { pattern: '**/*.ts', maxDepth: 3 });
// Returns: { totalFiles, truncated, filePaths }
How do I search file contents?
const hits = await ctx.manager.invoke('file-search', { pattern: 'TODO', include: '*.ts', contextLines: 2 });
// Returns: { matches: [{ file, line, content }] }
// outputMode: 'content' (default), 'files' (paths + counts), 'count' (totals)
All file operations enforce path traversal prevention.
Shell, Code & Fetch
How do I run shell commands?
const result = await ctx.manager.invoke('shell', { command: 'git status', timeout: 10000 });
// Returns: { stdout, stderr, exitCode }
How do I evaluate JavaScript?
const value = await ctx.manager.invoke('code', { code: 'return Object.keys(ctx.locals.config)' });
// Evaluates with ctx in scope. Supports await.
How do I fetch a URL?
const page = await ctx.manager.invoke('web-fetch', { url: 'https://example.com', searchTerms: 'pricing' });
// Returns: { url, title, content, truncated }
// Modes: 'selective' (default), 'truncated' (first 8000 chars), 'full'
Deploy
metadata:
deploy:
profiles:
my-app:
provider: apprunner # currently the only provider
region: us-east-1
port: 8080
instanceRole: arn:aws:iam::123456789012:role/my-role # for Bedrock access
env: [MY_API_KEY] # forward env vars
cpu: 1024 # 1 vCPU
memory: 2048 # MB
agent-apps deploy-app # build image, push to ECR, create App Runner service
agent-apps deploy-status # check status
agent-apps deploy-logs # fetch recent logs
agent-apps deploy-list # list all deployments
agent-apps deploy-remove # tear down
Sandbox
metadata:
sandbox:
profiles:
my-app:
engine: docker # currently the only engine
constraints:
fs: [{ path: ./data, mode: rw }]
network: { allow: [8080] }
env: [AWS_REGION]
resources: { memory: 512m, cpu: 1 }
agent-apps sandbox-build # build Docker image (base + project layers)
agent-apps sandbox-app --args '[]' # run main skill in container
agent-apps sandbox-list # list running containers
agent-apps sandbox-status # check container status
agent-apps sandbox-stop --name my-app # stop a container
Hub
agent-apps hub-search # browse all packages
agent-apps hub-search --query csv # keyword + semantic search
agent-apps hub-add --name echo # install latest
agent-apps hub-add --name echo@^1.0 # install version range
agent-apps hub-add --name echo --dryRun true # preview
agent-apps hub-remove --name echo # uninstall
agent-apps hub-update # update all
agent-apps hub-update --name echo # update one
agent-apps hub-list # installed + available updates
agent-apps hub-info --name echo # full package details
agent-apps hub-publish --path ./skills/my-skill.skill.js # publish to registry
Let agents self-service: allowed-tools: "* hub-search hub-add hub-remove hub-info"
Debugging
agent-apps --log level=trace hello # full invocation trace
agent-apps -vvvv hello # same (-v=warn, -vv=info, -vvv=debug, -vvvv=trace)
agent-apps --log agent hello # filter to agent scope
agent-apps --log scope=pipeline,agent hello # multiple scopes
agent-apps --log reporter=json hello # structured JSON output
agent-apps --dry-run hello # show config, don't execute
agent-apps --log # list all scopes and reporters
agent-apps skill-debug --ref hello # trace pipeline
agent-apps skill-debug --ref hello --breakpoints '["execute"]' # breakpoints
agent-apps skill-debug --ref hello --verbose true # args/result snapshots
agent-apps skill-profile --ref hello # timing data
Enable via metadata: metadata: { debug: true } or metadata: { profile: true }.
Troubleshooting
"No agent provider configured"
You need AWS credentials with Bedrock access and model config in your main skill. See AWS & Bedrock Setup.
Skill not found
- Check the name matches the filename (minus
.skill.js/.skill.md) - Make sure the directory is in your
pathsconfig - Run with
agent-apps --log level=debugto see discovery logs
Stale compiled tools
agent-apps skill-clean # wipe workspace/ and clear caches
Template literal escaping in inline code
In inline://code,... delegate refs (YAML), escape ${ as \${ to prevent metadata interpolation. Standalone .skill.js files are unaffected.
Desktop & VS Code
# Desktop editor — web-based, opens in browser
cd AgentAppsDesktop && npm start -- /path/to/project
# VS Code — extension activates when you open a project
# ⌘⇧P → "Agent Apps: Run Skill"
# @agent-apps in chat for help
# CodeLens: Run/Debug/Describe above each skill
Both find the CLI automatically: AGENT_APPS_ROOT → local node_modules → ~/.agent-apps/cli → PATH.
Key Concepts
What are the return value patterns?
return { status: 'ok', data: items }; // set ctx.locals.result, returned to caller
return { error: 'NOT_FOUND' }; // application error (not an exception)
throw new Error('Database failed'); // unexpected error (propagates through pipeline)
return undefined; // pipeline continues (next middleware runs)
How do I clean up resources in middleware?
import { ctxTarget } from 'agent-apps';
export default async function(ctx) {
const target = ctxTarget(ctx);
const conn = await db.connect();
target.locals.db = conn;
try {
await target.manager.next();
} finally {
conn.close(); // always runs, even on errors
}
}
How do I add tags to a skill?
metadata:
tags: [api, destructive, admin]
Query by tags: agent-apps skill-list --tags '["api", "admin"]' (AND logic). Tags are normalized to lowercase-with-hyphens.
How do I write good schema descriptions?
Property description fields are what agents read to understand tool behavior:
properties: {
path: {
type: "string",
description: "File path relative to project root. Returns raw string — parse JSON/YAML yourself."
}
}
Keep the top-level tool description to one sentence. Put behavior details, edge cases, and format notes in property descriptions.
- Everything is a skill. One interface:
(ctx, args?) => result. Middleware, agent, file I/O — all skills. - Search path wins. Same name, earlier in path → shadows the original. Workspace → project → library.
- Cascade order. Inherited → own keys → injected frontmatter →
--local→--global. ctx.manager—invoke(),finish(),fail(),next(),get(),set(),abort(),inject(),refresh(),log(),tail(),configure().- Middleware pattern. Read/write
ctx.envelope.target, calltarget.manager.next(). $prefix. Structural/meta keys:$inherit,$private,$public,$local,$global,$dynamic,$static,$merge,$order,$remove,$self,$hook.AGENT_APPS_ROOT— points to the runtime install. Used by CLI, Desktop, Extension.nonlocals.gateway— propagates to child invocations. Subagents reach the same user automatically.allowed-tools— top-level frontmatter field (not under metadata). Controls agent tool access.