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.

Email

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 paths config
  • Run with agent-apps --log level=debug to 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.managerinvoke(), finish(), fail(), next(), get(), set(), abort(), inject(), refresh(), log(), tail(), configure().
  • Middleware pattern. Read/write ctx.envelope.target, call target.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.

Ask AI