Skip to main content

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": { } }
}
FieldRequiredValue
apiVersionyesprovisioning.outermind.ai/v1. This is a version string, not a hostname. Any other value is 400 unsupported_api_version
kindyesAgentPackage. This is the document kind, and it is checked immediately after apiVersion. Any other value is 400 unsupported_kind
metadatayesIdentity of the package itself
specyesWhat 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."
}
FieldRequiredTypeNotes
nameyesstringThe 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
versionyesstringSemver, 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
descriptionnostringUp to 2000 characters. Human context for the package
exportedWithDriftnobooleanSet by the export route when it rendered live state that had drifted. Accepted on apply so an exported document round trips unchanged
lineagenoobjectWhere 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
}
FieldRequiredTypeDefaultNotes
friendlyNameyesstring1 to 200 characters. What the agent is called in the console
descriptionyesstring1 to 2000 characters. What the agent is for
instructionsyesstring1 to 100000 characters. The agent's operating instructions. Screened for safety at apply time
agentTypenoenumstandardstandard is the only accepted value in this version
supervisorEmailnostringAn address in this tenant. Must resolve to a real recipient
supervisorDisplayNamenostringUp to 200 characters
outbound.emailnoenuminternalnone, internal or any. How far the agent may send email
outbound.teamsnoenuminternalSame domain, for Teams
outbound.slacknoenuminternalSame domain, for Slack
limits.maxDailyUsageCostnonumber25Daily spend cap in dollars
limits.maxMonthlyUsageCostnonumber500Monthly spend cap in dollars
isActivenobooleantrueWhether the agent runs
autonomy.levelnoenumplatform default for the kindinvestigate_only, approve_all, auto_low, auto_medium or full
smenoobjectDeclares 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" }
]
}
}
FieldRequiredTypeNotes
enabledyesbooleanMust be true. The section's presence is the declaration
escalationContactyesstringThe Primary Escalation Contact: the person who receives this SME's escalation requests. Must resolve to a real recipient in the tenant
alternateEscalationContactnostringBackup recipient, and who an unanswered escalation escalates to
autoEscalateAfterHoursnointeger1 to 720. Hours before an unanswered escalation moves to the alternate contact
escalationTypesnoarrayRestricts 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
knowledgeAccessLevelnoenumown, department or all
toolDescriptionOverridenostring1 to 500 characters. How the SME is described to agents that might call it
defaultReminderHoursnointeger1 to 720
defaultTimeoutSecondsnointeger60 to 86400
maxNestingDepthnointeger1 to 10. How deep a chain of SME calls may go
invocationPolicynoobjectWhich 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.exceptionsnoarrayUp 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:

  1. spec.agent.supervisorEmail is required. An SME with no supervisor fails with sme_supervisor_required.
  2. spec.agent.description must be at least 80 characters. The description is what other agents search to discover this SME, so a one-liner makes it unfindable. Fails with sme_description_too_short.
  3. spec.designEvidence is 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 with sme_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.

FieldTypeNotes
purposestring1 to 4000 characters
objectivesarray of string1 to 50 entries, each up to 4000 characters
boundariesarray of string1 to 50 entries, each up to 4000 characters
successMeasuresarray of string1 to 50 entries, each up to 4000 characters
kpisarray of object1 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"] }
FieldTypeNotes
grantarray of stringUp 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
}
FieldRequiredTypeDefaultNotes
addressyesstringThe mailbox's SMTP address. It must already exist in Microsoft 365
mailboxTypenoenumsharedshared, individual or escalation
displayNamenostringUp to 255 characters
autoReplyEnablednobooleanfalseWhether the agent may reply without a human
rateLimitPerHournointegerCap on messages processed per hour
rateLimitPerDaynointegerCap on messages processed per day
processingRulesnoobjectOpaque 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" }
]
FieldRequiredTypeNotes
kindyesenumexistingRef is the only kind in this version
nameyesstring1 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" }
ValueBehaviour
request_human_inputThe agent asks a named person directly and waits for an answer
approval_queueThe agent's proposed action lands in the approval queue for review
mailbox_draftThe agent writes a draft into the mailbox for a human to send
noneNo 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.