The AgentPackage Document
An AgentPackage is the JSON document you POST to app/provisioning/agent-packages/apply. It describes one agent completely: its identity and instructions, the tools it may use, the mailbox it watches, the knowledge it can search, and how it asks a human for authority.
The machine-readable contract is served by GET app/provisioning/catalog/package-schema, and that is what you should validate against in a build pipeline. This page is the prose companion: what each field means and what to put in it.
Envelope
{
"apiVersion": "provisioning.outermind.ai/v1",
"kind": "AgentPackage",
"metadata": { "name": "billing-triage", "version": "1.0.0" },
"spec": { "agent": { } }
}
| Field | Required | Value |
|---|---|---|
apiVersion | yes | provisioning.outermind.ai/v1. This is a version string, not a hostname. Any other value is 400 unsupported_api_version |
kind | yes | AgentPackage. This is the document kind, and it is checked immediately after apiVersion. Any other value is 400 unsupported_kind |
metadata | yes | Identity of the package itself |
spec | yes | What the agent should be |
Unknown fields are rejected at every level. There is no "extra keys are ignored" behaviour anywhere in the document. A misspelled field name is 400 unknown_field with the offending path named, which is deliberately noisier than silently dropping a setting you thought you had applied.
No tenant identifier anywhere. A key spelled tenantId, at any depth and under any capitalisation, is 400 tenant_id_not_permitted_in_body, even when its value matches the key's own tenant.
metadata
{
"name": "billing-triage",
"version": "1.2.0",
"description": "Triages inbound billing questions for the finance shared mailbox."
}
| Field | Required | Type | Notes |
|---|---|---|---|
name | yes | string | The package's stable identity and the reconcile key. Lowercase letters, digits and hyphens, starting with a letter or digit, 2 to 63 characters. The agent's name is derived from this and is not separately settable |
version | yes | string | Semver, up to 20 characters. Recorded on each apply. It is not interpreted for ordering, so nothing stops you applying 1.0.0 after 2.0.0 |
description | no | string | Up to 2000 characters. Human context for the package |
exportedWithDrift | no | boolean | Set by the export route when it rendered live state that had drifted. Accepted on apply so an exported document round trips unchanged |
lineage | no | object | Where the package came from: sourceTemplateId, sourceVersion, origin, and sourceTenantId which is always null |
metadata.name is the identity the whole system reconciles on. Change it and you get a second agent, not a renamed one.
spec.agent
The only required section. Everything else is optional.
{
"friendlyName": "Billing Triage",
"description": "Reads inbound billing questions, identifies the account, and drafts a first response for review.",
"instructions": "You triage billing questions arriving in the finance shared mailbox...",
"supervisorEmail": "finance-lead@contoso.com",
"supervisorDisplayName": "Dana Okafor",
"outbound": { "email": "internal", "teams": "none", "slack": "none" },
"limits": { "maxDailyUsageCost": 10, "maxMonthlyUsageCost": 150 },
"autonomy": { "level": "auto_low" },
"isActive": true
}
| Field | Required | Type | Default | Notes |
|---|---|---|---|---|
friendlyName | yes | string | 1 to 200 characters. What the agent is called in the console | |
description | yes | string | 1 to 2000 characters. What the agent is for | |
instructions | yes | string | 1 to 100000 characters. The agent's operating instructions. Screened for safety at apply time | |
agentType | no | enum | standard | standard is the only accepted value in this version |
supervisorEmail | no | string | An address in this tenant. Must resolve to a real recipient | |
supervisorDisplayName | no | string | Up to 200 characters | |
outbound.email | no | enum | internal | none, internal or any. How far the agent may send email |
outbound.teams | no | enum | internal | Same domain, for Teams |
outbound.slack | no | enum | internal | Same domain, for Slack |
limits.maxDailyUsageCost | no | number | 25 | Daily spend cap in dollars |
limits.maxMonthlyUsageCost | no | number | 500 | Monthly spend cap in dollars |
isActive | no | boolean | true | Whether the agent runs |
autonomy.level | no | enum | platform default for the kind | investigate_only, approve_all, auto_low, auto_medium or full |
sme | no | object | Declares the agent a Subject Matter Expert. See below |
The outbound defaults are not narrow
outbound.email, outbound.teams and outbound.slack each default to internal when you omit them, because that is the platform's own column default. Omitting the section is therefore a request for internal, not a request for nothing, and it is checked against your key's ceiling on that basis. A key whose ceiling is none on a channel will refuse a document that omits outbound entirely. Declare "none" explicitly if that is what you want.
autonomy is sticky
Omitting autonomy leaves whatever the agent currently has. It never resets a level that a previous apply set. To change the dial, declare the new value.
spec.agent.sme
Declaring this section makes the agent a Subject Matter Expert: an agent other agents can call as a tool, with its own escalation path to a named human.
{
"enabled": true,
"escalationContact": "finance-lead@contoso.com",
"alternateEscalationContact": "controller@contoso.com",
"autoEscalateAfterHours": 24,
"escalationTypes": ["approval_required", "exception_request"],
"knowledgeAccessLevel": "department",
"defaultReminderHours": 8,
"defaultTimeoutSeconds": 3600,
"maxNestingDepth": 3,
"invocationPolicy": {
"mode": "explicit_callers",
"exceptions": [
{ "agentId": "3f2a91c4-8d1e-4b77-9a05-6c2e4f8b1d33", "effect": "allow" }
]
}
}
| Field | Required | Type | Notes |
|---|---|---|---|
enabled | yes | boolean | Must be true. The section's presence is the declaration |
escalationContact | yes | string | The Primary Escalation Contact: the person who receives this SME's escalation requests. Must resolve to a real recipient in the tenant |
alternateEscalationContact | no | string | Backup recipient, and who an unanswered escalation escalates to |
autoEscalateAfterHours | no | integer | 1 to 720. Hours before an unanswered escalation moves to the alternate contact |
escalationTypes | no | array | Restricts which request types this SME may escalate. Values: approval_required, clarification_needed, exception_request, compliance_concern, deadline_risk, resource_needed, information_request, other. Omit for no restriction, which is the platform default |
knowledgeAccessLevel | no | enum | own, department or all |
toolDescriptionOverride | no | string | 1 to 500 characters. How the SME is described to agents that might call it |
defaultReminderHours | no | integer | 1 to 720 |
defaultTimeoutSeconds | no | integer | 60 to 86400 |
maxNestingDepth | no | integer | 1 to 10. How deep a chain of SME calls may go |
invocationPolicy | no | object | Which agents may invoke this SME. mode is required and is either all_internal_eligible (every eligible internal agent) or explicit_callers (nobody except the allow exceptions). Omitting this block on the provisioning API resolves to explicit_callers with no exceptions, which is the restrictive mode and is deliberately NOT the console default. See the note below |
invocationPolicy.exceptions | no | array | Up to 100 per-caller overrides of { "agentId": "<uuid>", "effect": "allow" | "deny" }. An allow admits that caller under explicit_callers; a deny excludes it under all_internal_eligible. Each agentId must be a real, eligible agent in your own tenant, and one caller may appear at most once. The declared set replaces the live one on apply |
Three rules apply to an SME package, and all three are checked before anything is written:
spec.agent.supervisorEmailis required. An SME with no supervisor fails withsme_supervisor_required.spec.agent.descriptionmust be at least 80 characters. The description is what other agents search to discover this SME, so a one-liner makes it unfindable. Fails withsme_description_too_short.spec.designEvidenceis required when creating a new SME. An SME's design review needs an explicit operating contract. Re-applying over an existing SME does not require it, which is what lets an exported SME package be applied back unchanged. Fails withsme_design_evidence_required.
spec.mailbox and spec.agent.sme may be declared together. An SME that declares a mailbox monitors it exactly as a standard mailbox agent does, so it is reachable by email as well as through its generated sme_<name> tool. Omit spec.mailbox for an invocation-only SME, which remains the default. (This pairing used to be refused with sme_mailbox_exclusive; that error code no longer exists.)
An existing non-SME agent cannot be converted into an SME by re-applying its package with an sme section added. That fails with sme_conversion_not_supported; use a new package name, or convert the agent in the console.
Every settable sme field reconciles on re-apply. A changed setting is reported in the apply plan's changes as sme.<field> and converges the live agent. Omission is preserved per setting rather than per block: a document that declares sme but leaves out one of the scalar settings keeps that setting's live value rather than clearing it.
invocationPolicy is the one exception to that preservation rule, and it is worth reading twice. Because the provisioning API materialises explicit_callers whenever the block is absent, re-applying a document that declares sme without an invocationPolicy block does not leave a console-configured policy alone: it replaces it with explicit_callers and an empty exception list, so an SME that was invokable by every internal agent becomes invokable by none. Declare the block explicitly whenever the SME already exists, and remember that the declared exceptions set replaces the live one rather than merging into it.
Declaring sme also needs your key's ceiling to permit it (capabilities.smeAgents), the same way binding a mailbox does. The invocationPolicy.mode you request must additionally appear in capabilities.smeInvocationPolicyModes. That capability is empty on a ceiling that does not name it, so a key minted before it existed cannot apply an SME package at all until a replacement key is minted that grants the mode.
spec.designEvidence
Operator-authored evidence used to compile the agent's specification without the platform inventing objectives or boundaries on your behalf. Optional for most agents, required when creating a new SME.
{
"purpose": "Give the finance team a first-pass response to routine billing questions within one business hour.",
"objectives": ["Identify the account on every inbound question", "Draft a reply that cites the applicable policy"],
"boundaries": ["Never issue a refund", "Never quote a price not present in the policy documents"],
"successMeasures": ["A human sends the draft with light or no editing"],
"kpis": [
{ "metric": "resolution_rate", "target": 0.8, "direction": "maximize" },
{ "metric": "escalation_rate", "target": 0.15, "direction": "minimize" }
]
}
All five fields are required when the section is present.
| Field | Type | Notes |
|---|---|---|
purpose | string | 1 to 4000 characters |
objectives | array of string | 1 to 50 entries, each up to 4000 characters |
boundaries | array of string | 1 to 50 entries, each up to 4000 characters |
successMeasures | array of string | 1 to 50 entries, each up to 4000 characters |
kpis | array of object | 1 to 20 entries, each { metric, target, direction } |
kpis[].metric is one of resolution_rate, escalation_rate, approval_edit_rate, response_latency_seconds or tool_error_rate. kpis[].direction is maximize or minimize. kpis[].target is any number.
spec.tools
{ "grant": ["search_knowledge_base", "send_email"] }
| Field | Type | Notes |
|---|---|---|
grant | array of string | Up to 200 entries, no duplicates. Exact tool names only |
Wildcards are never accepted in a document. A key's ceiling may say search_*, but a package must say search_knowledge_base. A * in a grant fails with wildcard_not_allowed_in_package, which is a distinct reason from "this tool is not in your ceiling" so you can tell the two apart.
Names must be exactly what the tenant has. Read them from GET app/provisioning/catalog/tools, which also tells you which of them your key's ceiling covers.
A grant your tenant does not have is 422 preflight_failed with tool_not_available. A grant your key may not make is 409 package_exceeds_key_ceiling with tool_not_in_ceiling.
spec.mailbox
Binds the agent to a mailbox it monitors. Requires the mailboxes.bind scope, and a key ceiling with mailboxBinding set to true.
{
"address": "billing@contoso.com",
"mailboxType": "shared",
"displayName": "Billing",
"autoReplyEnabled": false,
"rateLimitPerHour": 60,
"rateLimitPerDay": 500
}
| Field | Required | Type | Default | Notes |
|---|---|---|---|---|
address | yes | string | The mailbox's SMTP address. It must already exist in Microsoft 365 | |
mailboxType | no | enum | shared | shared, individual or escalation |
displayName | no | string | Up to 255 characters | |
autoReplyEnabled | no | boolean | false | Whether the agent may reply without a human |
rateLimitPerHour | no | integer | Cap on messages processed per hour | |
rateLimitPerDay | no | integer | Cap on messages processed per day | |
processingRules | no | object | Opaque configuration passed through unchanged. Up to 100 top-level keys |
The API never creates mailboxes. The address must resolve to an existing shared or individual mailbox in your Microsoft 365 tenant; a distribution list is refused. If it does not resolve, apply fails with 422 preflight_failed and mailbox_not_found.
Binding also needs the tenant's Exchange authorization to be complete. If it is not, you get exchange_authorization_missing, which is fixed once in the console and then applies to every package.
spec.knowledgeSources
Attaches knowledge bases the tenant already has, so the agent can search them.
[
{ "kind": "existingRef", "name": "Billing Policies" }
]
| Field | Required | Type | Notes |
|---|---|---|---|
kind | yes | enum | existingRef is the only kind in this version |
name | yes | string | 1 to 200 characters. The knowledge base's exact display name |
Up to 50 entries, and names must be unique within one document.
The API never creates a knowledge index. existingRef attaches something an admin already built. Read the available names from GET app/provisioning/catalog/knowledge-bases; a name that does not match is 422 preflight_failed with knowledge_source_not_found.
spec.escalation
Names which shipped mechanism the agent uses when it wants human authority before acting.
{ "reviewPolicy": "request_human_input" }
| Value | Behaviour |
|---|---|
request_human_input | The agent asks a named person directly and waits for an answer |
approval_queue | The agent's proposed action lands in the approval queue for review |
mailbox_draft | The agent writes a draft into the mailbox for a human to send |
none | No review gate |
Setting anything other than none requires the escalation.configure scope, and the chosen policy must be in your key's ceiling. none is always permitted regardless of the ceiling: a key could not otherwise provision an agent that escalates to nobody, which is the narrowest thing a document can say.
request_human_input and approval_queue also need the tenant to have somewhere for the escalation to go. If it does not, apply fails with 422 preflight_failed and escalation_router_not_configured.
Defaults are materialised, not implied
Omitting a field with a default does not mean "leave it alone". The API fills the default in before it checks your key's ceiling, so an omitted field is checked as though you had written the default value. That is what makes the outbound behaviour above surprising the first time, and it is deliberate: a ceiling has to bound what the agent will actually end up with, not what you happened to type.
The full default set is served by GET app/provisioning/catalog/package-schema under defaults.
Validate against the served schema
curl -s -H "Authorization: Bearer $OM_KEY" \
"https://us-api.outermind.ai/api/app/provisioning/catalog/package-schema" \
| jq .schema > agent-package-v1.schema.json
Then validate with any JSON Schema draft-07 validator in your build. Two rules the schema cannot express are enforced separately, so a document a validator accepts may still be refused with a 400: the tenant identifier ban, and the requirement that knowledge-source names be unique within one document.