Are you an LLM? Read llms.txt for a summary of the docs, or llms-full.txt for the full context.
Skip to content

amodal.json

Every Amodal project starts with a single file: amodal.json at the root of your repo. This is the manifest. It tells the runtime who the agent is, which LLM to use, how to handle failover when a provider goes down, and how to connect to the outside world — MCP servers, data stores, and sandbox environments.

Think of it as package.json for an intelligent agent. The runtime reads it on startup, resolves any environment variable references, validates the schema, and uses it to bootstrap everything: the reasoning loop, provider connections, tool execution policies, and automation scheduling.

The config is deliberately flat. There are no nested pipelines or DAGs to configure. You pick your models, point at your connections, and the runtime handles the rest.

Minimal Config

The smallest thing that works. Three fields — name, version, and a main model:

{
  "name": "my-agent",
  "version": "0.1.0",
  "models": {
    "main": {
      "provider": "anthropic",
      "model": "claude-sonnet-4-20250514"
    }
  }
}

This gives you a working agent with no connections, no knowledge base, and no automations. It can chat, reason, and use built-in tools. You would use this during early development or when prototyping a new agent before wiring up APIs.

The provider field tells the runtime which SDK adapter to load. The model field is passed directly to that provider's API. The runtime expects the provider's API key in the standard environment variable (ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.) unless you specify explicit credentials.

Production Config with Failover

A realistic configuration for a deployed agent. This is what a production ops-agent might look like — primary model with a fallback, a cheaper model for data-gathering sub-agents, and custom system context:

{
  "name": "ops-agent",
  "version": "2.4.1",
  "description": "Infrastructure monitoring and incident response for Acme Corp",
 
  "models": {
    "main": {
      "provider": "anthropic",
      "model": "claude-sonnet-4-20250514",
      "credentials": {
        "api_key": "env:ANTHROPIC_API_KEY"
      },
      "fallback": {
        "provider": "openai",
        "model": "gpt-4o",
        "credentials": {
          "api_key": "env:OPENAI_API_KEY"
        }
      }
    },
    "simple": {
      "provider": "anthropic",
      "model": "claude-haiku-4-5-20251001",
      "fallback": {
        "provider": "google",
        "model": "gemini-2.0-flash"
      }
    }
  },
 
  "basePrompt": "You are the operations agent for Acme Corp. Our infrastructure runs on AWS (us-east-1 and eu-west-1). We use Datadog for monitoring, PagerDuty for incident management, and Slack (#ops-alerts) for communication. Always check Datadog metrics before escalating. Never restart production services without explicit confirmation."
}

The basePrompt string replaces the default system prompt entirely. Use it when you want full control over the agent's instructions — your company's infrastructure layout, naming conventions, escalation policies, or behavioral constraints. When omitted, the platform compiles a default system prompt from your skills, knowledge, and connection surfaces. This is not the place for methodology (that goes in skills) or reference data (that goes in the knowledge base). Think of it as the agent's core identity and ground rules.

Config with MCP, Stores, and Sandbox

When you need external tool servers, persistent storage, and sandboxed execution:

{
  "name": "finops-agent",
  "version": "1.2.0",
  "description": "Financial operations analysis and reporting",
 
  "models": {
    "main": {
      "provider": "anthropic",
      "model": "claude-sonnet-4-20250514"
    },
    "simple": {
      "provider": "google",
      "model": "gemini-2.0-flash"
    }
  },
 
  "sandbox": {
    "shellExec": true,
    "template": "finops-sandbox-v2",
    "maxTimeout": 60000
  },
 
  "stores": {
    "backend": "postgres",
    "postgresUrl": "env:DATABASE_URL"
  },
 
  "proactive": {
    "webhook": "https://hooks.acme.com/amodal/finops"
  },
 
  "mcp": {
    "servers": {
      "github": {
        "transport": "stdio",
        "command": "uvx",
        "args": ["mcp-server-github"],
        "env": { "GITHUB_TOKEN": "env:GITHUB_TOKEN" }
      },
      "postgres": {
        "transport": "stdio",
        "command": "npx",
        "args": ["-y", "@modelcontextprotocol/server-postgres"],
        "env": { "DATABASE_URL": "env:ANALYTICS_DB_URL" }
      },
      "custom-tools": {
        "transport": "sse",
        "url": "https://tools.internal.acme.com/mcp",
        "headers": {
          "Authorization": "env:INTERNAL_TOOLS_TOKEN"
        }
      }
    }
  }
}

MCP (Model Context Protocol) servers extend your agent with additional tools beyond the built-in set. Each server entry declares how to connect — stdio (the runtime spawns the process), sse (Server-Sent Events over HTTP), or http (streamable HTTP transport). The tools exposed by these servers appear alongside built-in tools in the agent's tool list.

The sandbox block controls shell execution. When shellExec is true, the agent can run arbitrary commands. The template field specifies which sandbox image to use (pre-configured with your dependencies), and maxTimeout caps how long any single command can run.

The stores block configures the database backend used for both agent document stores (see Stores) and session persistence (conversation history across restarts). A PostgreSQL database is required — set DATABASE_URL in your .env file (or hosting environment). The store layer uses Drizzle ORM internally, so the schema is managed automatically.

Fields

Required

FieldTypeDescription
namestringAgent name (min 1 char). Used in logging and deployment IDs.
versionstringSemantic version. Used for snapshot tagging and versioning.

Optional

FieldTypeDescription
descriptionstringAgent description. Shown in CLI output.
basePromptstringCustom base system prompt. When set, replaces the platform-compiled default.
disabledSubagentsstring[]Subagent names to disable (e.g., ['explore', 'plan'] to turn off platform defaults).
models.mainModelConfigPrimary agent model. When omitted, auto-detects from environment API keys (prefers Google, then Anthropic, OpenAI, etc.).
models.simpleModelConfigModel for data-gathering sub-agents. Should be faster and cheaper than main.
models.advancedModelConfigModel for complex analysis tasks that need stronger reasoning than main.
sandboxobjectSandbox execution config. Controls whether custom tool handlers can call ctx.exec() for shell commands, and how sandboxing is enforced.
storesobjectData store backend config. Requires Postgres via DATABASE_URL.
proactiveobjectWebhook URL for external triggers (automation webhooks, third-party integrations).
mcpobjectMCP server connections. Each server exposes additional tools to the agent.
webToolsobjectEnables web_search + fetch_url built-in tools via Gemini grounding. See Web Tools.
packagesstring[]Installed agent packages to load. This is the canonical field for connection, skill, knowledge, tool, channel, and page packages.
memoryobjectPersistent memory across sessions. See Memory.
fileToolsboolean or objectEnable built-in file tools for reading/writing agent repo files. Set to true for defaults, or pass {allowedDirs, blockedFiles} to customize. Used by the admin agent.
scopeobjectPer-user isolation for multi-tenant deployments. See Scope.

packages is the only runtime package declaration field. Use it for marketplace or npm packages that ship connections, skills, tools, knowledge, pages, stores, automations, or channels. Standard npm dependencies belong in package.json; they are not read from amodal.json.

ModelConfig

{
  "provider": "anthropic" | "openai" | "google",
  "model": "claude-sonnet-4-20250514",
  "region": "us-east-1",            // optional, provider region
  "baseUrl": "https://...",          // optional, custom endpoint
  "credentials": {                   // optional, explicit keys
    "api_key": "env:ANTHROPIC_API_KEY"
  },
  "fallback": { ... }               // optional, another ModelConfig
}

Model Tiers: Main vs. Simple vs. Advanced

The main model is your reasoning engine. It handles the user conversation, decides when to dispatch sub-agents, interprets their findings, plans next steps, and composes the final response. This is where model quality matters most — you want the best model you can afford here, because this is where judgment happens.

The simple model is the workhorse. When the primary agent dispatches a task agent to gather data — "go query Datadog for the last hour of CPU metrics" or "pull the customer's recent Stripe invoices" — that task agent uses the simple model. These sub-agents do focused, bounded work: load some knowledge, make API calls, interpret the raw response, and return a clean summary. They do not need the full reasoning capability of the main model.

The advanced model is for complex analysis that needs stronger reasoning than the main model. Use this for tasks requiring deep multi-step reasoning, such as compliance auditing, complex financial analysis, or code review.

This matters for cost and latency. A complex investigation might dispatch 5-10 task agents, each making multiple tool calls. If every one of those runs on your most expensive model, costs add up fast and the user waits longer. By routing sub-agents to a faster, cheaper model, you keep the primary agent's context clean (it only sees the summaries) and your token bill reasonable.

Typical pairings:

Use CaseMainSimple
Cost-optimizedClaude SonnetClaude Haiku
Quality-firstClaude OpusClaude Sonnet
Multi-providerClaude SonnetGemini Flash

If you omit models.simple, the runtime falls back to models.main for everything. This works fine — it just costs more and runs slower on complex questions.

Fallback Chains

Every ModelConfig can include a fallback — another ModelConfig that the runtime tries when the primary provider fails. Failures include HTTP 5xx errors, rate limits (429), timeouts, and authentication errors.

The fallback is itself a full ModelConfig, which means it can have its own fallback, forming a chain:

{
  "models": {
    "main": {
      "provider": "anthropic",
      "model": "claude-sonnet-4-20250514",
      "fallback": {
        "provider": "openai",
        "model": "gpt-4o",
        "fallback": {
          "provider": "google",
          "model": "gemini-2.5-pro"
        }
      }
    }
  }
}

In this example: the runtime tries Anthropic first. If that is down (outage, rate limit), it falls back to OpenAI GPT-4o. If OpenAI also fails, it falls back to Gemini. The user never sees the failover — the runtime handles it transparently and logs the switch.

Fallback is per-request. If Anthropic recovers, the next request goes back to the primary. There is no sticky routing.

This is particularly useful for production deployments where uptime matters more than provider loyalty. A multi-provider fallback chain means your agent stays up even during provider outages, which happen more often than you would like.

Environment Variables

Any string value in amodal.json can reference an environment variable using the env: prefix:

{
  "stores": {
    "postgresUrl": "env:DATABASE_URL"
  },
  "mcp": {
    "servers": {
      "github": {
        "env": { "GITHUB_TOKEN": "env:GITHUB_TOKEN" }
      }
    }
  }
}

The runtime resolves these at parse time — before any connections are established or tools are loaded. If a referenced variable is missing, the runtime throws an ENV_NOT_SET error and refuses to start. This is intentional. A misconfigured agent is worse than a stopped agent.

When to Use env: vs. Hardcoded Values

Always use env: for:
  • API keys, tokens, and secrets of any kind
  • Database connection strings (they contain passwords)
  • Anything that changes between environments (dev/staging/prod)
Hardcode when:
  • The value is not sensitive: model names, project IDs, region strings, cron schedules
  • The value is part of the agent's identity: name, version, description

A good rule of thumb: if you would not want the value visible in a public GitHub repo, use env:. The amodal.json file is checked into git. Secrets should never be in it directly.

For local development, put your environment variables in a .env file at the repo root (and add it to .gitignore). The runtime loads .env automatically in repo mode. For production deployments, set them in your hosting environment's secret management — Kubernetes secrets, AWS Parameter Store, Fly.io secrets, or whatever your infrastructure uses.

Memory

Enable persistent memory so the agent can remember facts across sessions:

{
  "memory": {
    "enabled": true
  }
}

When enabled, the agent gets a built-in memory tool with add, remove, list, and search actions. Memory entries are stored as individual rows in the database, scoped by agent (and optionally per user via scope_id). On each new session, existing entries are injected into the system prompt automatically.

FieldDefaultDescription
enabledRequired. Set to true to enable memory.
maxEntries50Maximum number of memory entries.
maxTotalChars8000Maximum total characters across all entries.
editableBy"any"Who can call the memory tool: "any", "admin", or "none".
nudgeInterval10Prompt the agent to save every N turns. Set to 0 to disable.
sessionSearchtrueEnable the session search tool for querying past sessions.

Scope

For ISVs embedding the agent in a multi-tenant app, scope_id gives each end user isolated memory, store partitions, and session history.

{
  "scope": {
    "requireScope": true
  }
}

Setting requireScope: true rejects any request that does not include a scope_id. Recommended for production multi-tenant deployments.

Passing a scope ID:
  • Local dev / unauthenticated: include scope_id in the chat request body
  • Cloud / JWT auth: include scope_id in JWT claims (takes precedence over body)

Shared stores: By default, stores are partitioned per scope. To make a store shared across all scopes, add "shared": true to the store JSON file.

Context injection: Connection specs support a contextInjection map to automatically forward scope context values (passed with the request) into API calls — as query params, headers, path variables, or body fields. See Connections — Context Injection for full configuration details.

Per-scope credentials: Use the scope:KEY prefix in connection auth/header values to read credentials from the scope's secrets map. In local dev, define these in .amodal/scopes.json keyed by scope ID.

Studio visibility: Studio Sessions and Cost & Usage use scope_id to filter sessions and compare spend across tenants, users, or workspaces. Amodal stores the raw stable ID; your application owns any display-name mapping. See Embedding & Multi-tenancy — Viewing Scope Usage in Studio.

Agent Loop

The agent loop runs up to maxTurns (default 50) turns per user message. Each turn is one LLM call that may produce tool calls or text. This is the primary safety limit.

Tool repeat limit

By default there is no limit on how many times the agent can call the same tool in a single response. If you want to cap repeated tool calls (e.g., to prevent an agent from making 100 API calls in one go), set maxToolRepeats in amodal.json:

{
  "agent": {
    "maxToolRepeats": 20
  }
}

When the limit is hit, the agent stops and says "I've made N calls, here's what I have so far. Say 'keep going' if you'd like me to continue." The counter resets on the next user message.

SettingDefaultDescription
maxTurns50Total LLM calls per user message. The overall budget.
maxToolRepeats0 (no limit)Max times the same tool can be called with similar parameters in one response. 0 = unlimited.