Contents · 17 sections↓
- 01How do you declare an AI agent in a YAML manifest?
- 02What is an AI agent manifest?
- 03When to use a .agent manifest (and when not to)
- 04What you need
- 05Step 1: Write a minimal manifest and validate it
- 06Step 2: Give the agent tools and triggers
- 07Step 3: Set state, memory and the journal
- 08Step 4: Add governance and process settings
- 09Step 5: Run commands on lifecycle events with hooks
- 10Step 6: Resolve environment variables with ${VAR}
- 11Step 7: Catch typos with strict validation
- 12The complete example
- 13Running it in production
- 14Common problems and fixes
- 15Limitations
- 16Frequently asked questions
- 17Next steps
An AI agent manifest is a YAML file that declares a long-running agent: its model and instructions, the tools it may use, what wakes it up, the state it keeps and the limits it runs under. This guide is for developers who run agents unattended, on schedules, webhooks or file changes. You will write a .agent manifest section by section, validate it, start it in the Promptise Foundry Agent Runtime, and learn how strict validation, ${VAR} references and lifecycle hooks work in Promptise 1.5.0.
How do you declare an AI agent in a YAML manifest?
Write one YAML file per agent process. Give it a name, a model and instructions, list its MCP tool servers under servers, and say what starts a run under triggers. Then add the sections you need: world and context for state, journal for crash recovery, secrets, budget, health and mission for governance, hooks for commands that run on lifecycle events, and config for process settings such as the restart policy. Check the file with promptise runtime validate and start it with promptise runtime start. Every key is checked, so a typo stops the load instead of being ignored.
version: "1.0"
name: hello-agent
model: openai:gpt-5-mini
instructions: |
You write a one-line status note for the team each weekday morning.
triggers:
- type: cron
cron_expression: "0 7 * * 1-5"Key takeaways:
A manifest declares a process, not a single call. It says what wakes the agent, what it keeps between runs and how it recovers.
Validation is strict. An unknown key in any section, trigger, server or hook fails the load, and in most sections the message names the key you probably meant.
`${VAR}` references resolve when the file loads, except secret values and hook environment values, which stay references until they are used.
Manifests journal by default. A process loaded from a manifest writes checkpoints to .promptise/journal and continues from them when it starts again.
YAML can't hold Python objects. Callback hooks, guardrail scanners and custom success checks stay in code.
[02]
What is an AI agent manifest?
A chat agent lives for one request. A long-running agent is a process: it waits for a schedule or an event, runs, keeps some state and does that again for weeks, so it has more to declare than a prompt and a tool list. It needs a trigger, a place for its state, a record of what it did, and limits that hold when nobody watches. An AI agent manifest puts all of that in one file you can review, version and check before anything runs, much like a Kubernetes Deployment or a systemd unit describes a service.
The terms this guide uses:
Process: one long-running agent with a lifecycle (created, running, suspended, stopped, failed).
Trigger: what starts a run, such as a cron schedule, a webhook, a file change or an event.
Context state: a small key-value store the process keeps between runs.
Journal: an append-only log of state changes, tool calls and checkpoints, used for crash recovery and rewind.
Governance: limits and checks per process: scoped secrets, budgets, health monitoring and missions.
What is an agentic runtime? explains how these parts fit together.
How Promptise loads a manifest
agents/pipeline-watcher.agent
│
▼
1. read the YAML ────────────────── ManifestError: no file, invalid YAML, not a mapping
│
▼
2. resolve ${VAR} references ────── EnvVarNotFoundError: variable unset, no default
(secrets.secrets and hooks[].env
stay references for later)
│
▼
3. strict schema check ──────────── ManifestValidationError: unknown key ("did you
every section, trigger, mean ...?"), wrong type, option set twice
server, hook and identity
│
▼
4. manifest_to_process_config() ─── ProcessConfig (with a journal unless you turn it off)
│
▼
5. AgentRuntime.add_process() ───── AgentProcess, started by `promptise runtime start`promptise runtime validate runs steps 1 to 3. promptise runtime start runs all five and then starts the process. These are the top-level keys the schema accepts; only name is required:
Key | Default | Sets |
|---|---|---|
version | "1.0" | Schema version; any other value is rejected |
name | required | The process name |
model | openai:gpt-5-mini | The model string |
instructions | none | The system prompt |
identity | none | Who the agent is, for attribution and signed credentials |
servers | | MCP servers (HTTP or stdio) |
triggers | | What starts a run |
world | | Initial context state |
context | defaults | Any ContextConfig field |
memory | off | Long-term memory, in short key names |
journal | checkpoint journal | Journal level, backend, path, recovery |
execution_mode, open_mode | strict | Whether the agent may change itself, and its limits |
secrets, budget, health, mission | off | Governance sections, each with enabled: true |
hooks | | External commands run on lifecycle events |
config | | Any other process setting |
entrypoint | none | Not read; setting it emits a FutureWarning |
[03]
When to use a .agent manifest (and when not to)
Promptise has three kinds of configuration file, and they describe different things:
File | Describes | Run it with | Use it when |
|---|---|---|---|
.agent | One long-running process: triggers, state, journal, governance, hooks | promptise runtime start | The agent wakes on schedules, webhooks, files or events and runs unattended |
.superagent | One agent: model, instructions, servers, memory, sandbox, approval, other agents | promptise agent or build_superagent() | You call the agent yourself, from a chat, a request or a script |
promptise.toml | A project: the main program, scripts, environment, dev and production settings | promptise run | The agent lives in Python code and you want dev tools, a terminal UI or an HTTP API |
AI agent YAML config files covers .superagent files, and run, debug and deploy an AI agent covers promptise.toml.
Use a ProcessConfig in Python instead of a manifest when the process needs objects YAML can't express: a callback hook, a guardrail scanner, an approval policy with its handler, a semantic cache or a mission success_check function. A manifest is the better choice when people other than the author review agent changes, when operators deploy agents without editing code, or when one runtime loads a directory of agents.
[04]
What you need
Python 3.10 or later and Promptise Foundry 1.5.0 or later.
An OpenAI API key in OPENAI_API_KEY for the one run in step 5. The example uses openai:gpt-5-mini; one run costs a fraction of a cent. The other steps make no model calls.
About 20 minutes.
pip install "promptise>=1.5.0"The example project, a pipeline watcher with one MCP tool server and one hook script:
pipeline-agents/
├── agents/
│ └── pipeline-watcher.agent # the manifest (complete example below)
├── hooks/
│ └── log_event.py # a lifecycle hook (step 5)
├── pipeline_tools.py # MCP server: pipeline_status, create_alert
└── .env # PIPELINE_WEBHOOK_SECRET, PAGER_TOKEN[05]
Step 1: Write a minimal manifest and validate it
Save the manifest from the answer above as agents/hello.agent and validate it:
$ promptise runtime validate agents/hello.agent
.env loaded from /home/me/pipeline-agents/.env
Validating agents/hello.agent...
✓ Schema validation passed
⚠ No MCP servers configured — agent has no tools
Manifest Summary
┏━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ Field ┃ Value ┃
┡━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Name │ hello-agent │
│ Model │ openai:gpt-5-mini │
│ Version │ 1.0 │
│ Instructions │ You write a one-line status note for the team each weekday m... │
│ Servers │ 0 │
│ Triggers │ 1 │
└──────────────┴─────────────────────────────────────────────────────────────────┘
✓ Validation complete!
[exit 0](The [exit N] lines in this guide's output show each command's exit code; the capture script adds them.) The schema check passed. The warning is advice, not an error: validate warns when a manifest has no instructions, no triggers or, as here, no MCP servers. The .env loaded from line shows that the promptise CLI read a .env file before the manifest. Step 6 explains why that matters.
Three details trip people up in the first lines. version must be the string "1.0": unquoted, YAML reads 1.0 as a number and the schema rejects it. name becomes the process name, which promptise runtime start --name can override for a single file. And a directory is loaded by its *.agent files only, so keep the extension.
[06]
Step 2: Give the agent tools and triggers
The servers section uses the same format as .superagent files. A stdio server is a program the process starts; an HTTP server is a URL. Each entry is checked whole, so a misspelt key fails:
servers:
pipeline_tools: # a local program, started with the process
type: stdio
command: python
args: ["pipeline_tools.py"]
env: {PIPELINE_ENV: production}
tickets: # a remote MCP server
type: http
url: https://tickets.internal.example.com/mcp
bearer_token: ${TICKETS_TOKEN}type is http, streamable-http, sse or stdio. An HTTP entry needs url and can set headers, bearer_token, api_key and audience (for identity credentials). A stdio entry needs command and can set args, env, cwd and keep_alive. Relative paths, like pipeline_tools.py here, resolve against the directory you start the runtime in, not the manifest's folder. The example's tool server is a short Promptise MCP server:
"""MCP tools for the pipeline watcher: read pipeline status, open an alert."""
import json
from datetime import datetime, timezone
from pathlib import Path
from promptise.mcp.server import MCPServer
server = MCPServer("pipeline-tools")
PIPELINES = {
"orders-etl": {"status": "healthy", "lag_minutes": 2},
"billing-sync": {"status": "degraded", "lag_minutes": 47},
"search-index": {"status": "healthy", "lag_minutes": 5},
}
ALERTS = Path("alerts.jsonl")
@server.tool(read_only_hint=True)
async def pipeline_status() -> dict:
"""Current status and lag (minutes) of every data pipeline."""
return PIPELINES
@server.tool()
async def create_alert(pipeline: str, summary: str) -> dict:
"""Open an alert for a degraded pipeline."""
alert = {
"pipeline": pipeline,
"summary": summary,
"at": datetime.now(timezone.utc).isoformat(timespec="seconds"),
}
with ALERTS.open("a") as fh:
fh.write(json.dumps(alert) + "\n")
return {"status": "created", **alert}
if __name__ == "__main__":
server.run(transport="stdio")The triggers section is a list. Each entry has a type and the fields of that type:
triggers:
- type: cron
cron_expression: "*/15 * * * *"
cron_timezone: Europe/Zurich
- type: webhook
webhook_path: /pipeline-events
webhook_port: 9091
hmac_secret: ${PIPELINE_WEBHOOK_SECRET}
- type: file_watch
watch_path: /data/inbox
watch_patterns: ["*.csv"]
watch_backend: polling
- type: event
event_type: pipeline.degradedEach trigger is checked against its type when the manifest loads: an unknown key fails, and so does an option of another type, such as watch_path on a cron trigger. Webhook triggers listen on loopback unless you set webhook_host, and the port must be above 1024. The guides on cron schedules and webhook triggers cover the options of the two most common types in depth.
[07]
Step 3: Set state, memory and the journal
These sections decide what the process remembers between runs:
world:
open_alerts: []
context:
writable_keys: [open_alerts]
state_tool: true
conversation_max_messages: 40
memory:
provider: chroma
collection: pipeline-watcher
persist_directory: .promptise/memory
max: 5
journal:
level: full
path: .promptise/journal
recover_on_start: trueworld is the initial context state. context accepts any ContextConfig field: writable_keys limits what the agent may change (empty means every key), state_tool: true gives the agent an update_state(key, value) tool, conversation_max_messages caps the history kept between runs (default 100), and env_prefix (default AGENT_) and file_mounts expose environment variables and files. You can put the initial state in context: initial_state instead of world, but not in both. AI agent state between runs explains context state in detail.
memory uses short names for the memory_* fields: provider (in_memory, chroma or mem0), collection, persist_directory, user_id, auto_store, max, min_score and timeout. Set an option here or as memory_* under context, not both.
journal sets level (none, checkpoint or full), backend (file or memory), path and recover_on_start. Here manifests differ from code on purpose: a manifest without a journal section still gets a checkpoint journal in .promptise/journal, with recovery on start, while a ProcessConfig built in Python has no journal unless you add one. Set journal: {level: none} if each start should be fresh. Durable AI agents covers recovery.
[08]
Step 4: Add governance and process settings
Governance sections are off until you set enabled: true:
secrets:
enabled: true
secrets:
pager_token: ${PAGER_TOKEN}
default_ttl: 3600
budget:
enabled: true
max_tool_calls_per_run: 10
max_runs_per_day: 200
on_exceeded: pause
health:
enabled: true
stuck_threshold: 3
on_anomaly: escalate
escalation:
webhook_url: https://hooks.example.com/agent-alerts
mission:
enabled: true
objective: Bring billing-sync lag under 10 minutes.
success_criteria: pipeline_status reports billing-sync healthy.
eval_every: 5
config:
heartbeat_interval: 15
idle_timeout: 3600
max_consecutive_failures: 5
restart_policy: on_failure
max_restarts: 3
max_invocation_time: 120
inbox:
enabled: true
trigger_delivery:
max_retries: 1
dead_letter_size: 200Each section takes the fields of its config class: SecretScopeConfig, BudgetConfig, HealthConfig and MissionConfig. Scoped secrets and budgets have their own guides, and the autonomous AI agent runtime guide shows how health checks and missions work with them. In 1.5.0, on_anomaly: escalate always suspends the process and sends the notification only when escalation is set.
config takes every process setting that has no section of its own. The defaults are a heartbeat every 10 seconds, no idle timeout and no lifetime limit (0), failed after 3 consecutive failures, restart_policy: never with 3 restarts and a 1-second backoff, no run timeout, and one run at a time (concurrency: 1). trigger_delivery sets retries (0 by default) and the dead-letter list (100 events), and inbox turns on messages to the running agent. A key that has its own section, such as instructions, is refused in config when that section is also set.
Two more sections change what the agent is. identity gives the process a stable identity, and execution_mode: open lets it change its own instructions, tools and triggers within the limits under open_mode:
identity:
provider: local
agent_id: pipeline-watcher
owner: data-platform
execution_mode: open
open_mode:
allow_identity_change: true
allow_tool_creation: false
allow_mcp_connect: false
allow_trigger_management: true
max_dynamic_triggers: 3Set allow_tool_creation: false unless you accept model-written Python running in the runtime's process; it is on by default in open mode. The meta-tools docs explain each permission.
[09]
Step 5: Run commands on lifecycle events with hooks
The hooks section runs an external program when something happens to the process. The complete manifest declares two hooks:
hooks:
- event: session_start
command: ["python", "hooks/log_event.py"]
- event: task_completed
command: ["python", "hooks/log_event.py"]
timeout: 5The program gets the event as JSON on stdin. This one appends a line to hooks.log:
"""Lifecycle hook: append the event the runtime sends on stdin to hooks.log."""
import json
import sys
event = json.load(sys.stdin)
data = event["data"]
line = f'{event["timestamp"][:19]} {event["process_id"]} {event["event"]} {json.dumps(data)}'
with open("hooks.log", "a") as fh:
fh.write(line + "\n")
# No output: the event goes on unchanged.To see the whole manifest work, this script loads it, starts the process, sends it one event and stops it:
"""Start the manifest's process, send it one event, then stop it cleanly."""
import asyncio
import json
from pathlib import Path
from promptise.runtime import AgentRuntime, TriggerEvent
async def main() -> None:
async with AgentRuntime() as runtime: # stop_all() when the block ends
process = await runtime.load_manifest("agents/pipeline-watcher.agent")
await runtime.start_process(process.name)
print("state after start:", process.state.value)
await process.inject(TriggerEvent(
trigger_id="manual", trigger_type="manual", payload={"reason": "first check"},
))
while process.status()["invocation_count"] < 1:
await asyncio.sleep(0.5)
await asyncio.sleep(1) # let the task_completed hook finish
print("open_alerts:", process.context.get("open_alerts"))
print("state after stop:", process.state.value)
print("\nalerts.jsonl:")
for line in Path("alerts.jsonl").read_text().splitlines():
print(" ", json.loads(line)["pipeline"], "-", json.loads(line)["summary"])
print("\nhooks.log:")
print(Path("hooks.log").read_text().rstrip())
asyncio.run(main())state after start: running
open_alerts: ['billing-sync']
state after stop: stopped
alerts.jsonl:
billing-sync - Pipeline degraded: lag 47 minutes
hooks.log:
2026-10-20T20:49:10 pipeline-watcher session_start {"restart": false, "recovered": null}
2026-10-20T20:49:21 pipeline-watcher task_completed {"event_id": "ccaae147-2d44-4f03-b117-44d4b37b5643", "trigger_id": "manual", "trigger_type": "manual", "outcome": "success", "duration_ms": 10332.3, "error": null}The agent called pipeline_status, opened one alert and saved the pipeline name with update_state. The session_start hook ran when the process entered running, and task_completed ran after the run, with its outcome and duration. Start the same manifest again, and session_start reports what the default journal restored:
recovered: {'lifecycle_state': 'stopped', 'entries_replayed': 3, 'context_keys': ['open_alerts'], 'mission_restored': False, 'budget_restored': True, 'conversation_messages': 2, 'invocation_count': 1}
open_alerts: ['billing-sync']
last hook: 2026-10-20T20:49:24 pipeline-watcher session_start {"restart": false, "recovered": {"lifecycle_state": "stopped", "entries_replayed": 3, "context_keys": ["open_alerts"], "mission_restored": false, "budget_restored": true, "conversation_messages": 2, "invocation_count": 1}}Each hook entry takes event (one of 13 lifecycle events), command, timeout (5 seconds by default for a command), priority, once, tag, fail_closed, cwd, env, env_passthrough and max_output_bytes (64 KiB). No shell runs the command: a list is used as is, and a string is split like a terminal would split it. The program gets a minimal environment, not your shell's, so pass what it needs with env_passthrough or env. A program can also answer with JSON to block an action or rewrite a prompt, and fail_closed: true makes a failing policy hook block instead of letting the action through. A callback: key is an error in a manifest: Python callbacks are registered in code. AI agent lifecycle hooks covers the events and their data.
[10]
Step 6: Resolve environment variables with ${VAR}
Keep credentials and machine-specific values out of the file with references. ${VAR} and ${VAR:-default} work in any string value, including instructions, and resolve when the manifest loads. A reference to an unset variable without a default fails the load. Two kinds of value stay references:
`secrets.secrets` values are resolved by the process's secret scope when it starts, so the loaded config never holds the secret.
`hooks[].env` values are resolved each time the hook runs.
For both, a missing variable still fails at load time, so you learn about it before the process starts. Every other value, such as a webhook's hmac_secret or a server's bearer_token, holds the real value after loading. Secret-bearing fields keep it masked in repr() and JSON dumps. This script shows what the runtime builds from the complete manifest:
"""Load a manifest and print the ProcessConfig the runtime builds from it."""
import sys
from promptise.runtime import load_manifest, manifest_to_process_config
path = sys.argv[1] if len(sys.argv) > 1 else "agents/pipeline-watcher.agent"
manifest = load_manifest(path)
config = manifest_to_process_config(manifest)
print("name: ", manifest.name)
print("model: ", config.model)
print("identity: ", type(config.identity).__name__, config.identity.agent_id)
print("servers: ", {n: type(s).__name__ for n, s in config.servers.items()})
for t in config.triggers:
print("trigger: ", t.type, t.cron_expression or t.webhook_path, "hmac:", t.hmac_secret)
print("initial state: ", config.context.initial_state)
print("writable keys: ", config.context.writable_keys, "state_tool:", config.context.state_tool)
print("journal: ", config.journal.level, config.journal.backend, config.journal.path,
"recover_on_start:", config.journal.recover_on_start)
print("secret template: ", manifest.secrets["secrets"])
print("secrets config: ", config.secrets.secrets)
print("budget: ", config.budget.max_tool_calls_per_run, "tool calls/run,",
config.budget.max_runs_per_day, "runs/day, on_exceeded:", config.budget.on_exceeded)
print("hooks: ", [(h.event, h.command, h.timeout) for h in config.hooks])
print("restart policy: ", config.restart_policy, "max_restarts:", config.max_restarts)
print("retries: ", config.trigger_delivery.max_retries)
print("defaults kept: ", "concurrency", config.concurrency, "| idle_timeout", config.idle_timeout)$ python inspect_manifest.py
name: pipeline-watcher
model: openai:gpt-5-mini
identity: AgentIdentity pipeline-watcher
servers: {'pipeline_tools': 'StdioServerSpec'}
trigger: cron */15 * * * * hmac: None
trigger: webhook /pipeline-events hmac: **********
initial state: {'open_alerts': []}
writable keys: ['open_alerts'] state_tool: True
journal: full file .promptise/journal recover_on_start: True
secret template: {'pager_token': '${PAGER_TOKEN}'}
secrets config: {'pager_token': SecretStr('**********')}
budget: 10 tool calls/run, 200 runs/day, on_exceeded: pause
hooks: [('session_start', ['python', 'hooks/log_event.py'], None), ('task_completed', ['python', 'hooks/log_event.py'], 5.0)]
restart policy: on_failure max_restarts: 3
retries: 1
defaults kept: concurrency 1 | idle_timeout 0.0
[exit 0]The secret template is still ${PAGER_TOKEN}, the webhook secret is resolved but masked, and the defaults you didn't set are filled in. Where do the values come from? Every promptise command loads a .env file from the working directory, or from a parent up to the project root (the first folder with pyproject.toml or .git), and never overrides a variable that is already set. PROMPTISE_NO_DOTENV=1 turns that off. load_manifest() in your own Python code reads only the process environment. Without the variables, validation stops at the first missing one:
$ env -u PAGER_TOKEN -u PIPELINE_WEBHOOK_SECRET promptise runtime validate agents/pipeline-watcher.agent
Validating agents/pipeline-watcher.agent...
╭─────────────────────────────── Traceback (most recent call last) ────────────────────────────────╮
…
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
EnvVarNotFoundError: Environment variable 'PAGER_TOKEN' not found (referenced in:
secrets.secrets.pager_token)
[exit 1]In 1.5.0 this error prints with a traceback. The last line is the message to act on.
[11]
Step 7: Catch typos with strict validation
Before 1.5.0, a misspelt key inside a section such as health was dropped silently, and the option kept its default. Now the loader rejects unknown keys everywhere. Here are three real mistakes and what promptise runtime validate says:
$ promptise runtime validate broken/2-section.agent
…
✗ Schema validation failed:
Manifest validation failed: 1 validation error for AgentManifestSchema
Value error, health: unknown key(s) ['error_windw']; valid keys are ['cooldown',
…
'escalation', 'loop_min_repeats', 'loop_window', 'on_anomaly', 'stuck_threshold'] (did you mean
'error_windw' -> 'error_window'?) [type=value_error, input_value={'name': 'pipeline-watche...rue,
…
$ promptise runtime validate broken/4-trigger.agent
…
Value error, triggers[0] (cron): unknown key(s) ['cron_expresion']; valid keys are
…
you mean 'cron_expresion' -> 'cron_expression'?) [type=value_error, input_value={'name':
…
$ promptise runtime validate broken/7-twice.agent
…
Value error, config: ['instructions'] also set by the manifest's own section(s); set each option
in one place (the top-level section) [type=value_error, input_value={'name': 'pipeline-watche...he
…
[exit 1]Where the message names a likely key and where it only lists the valid ones:
Where the typo is | What the error gives you |
|---|---|
context, journal, open_mode, secrets, budget, health, mission | The valid keys and a "did you mean" hint |
A trigger, a hook, identity | The valid keys and a "did you mean" hint |
config and memory | The valid keys, no hint |
A top-level key (journal_level) |
|
A server entry |
|
A hook event (on_start) | The 13 valid event names |
Validate the whole directory in CI: promptise runtime validate agents/ checks every *.agent file, ends with ✗ N of M manifest(s) failed, and exits with 1. For full coverage, also run manifest_to_process_config(load_manifest(path)) in a test, which builds the complete ProcessConfig the way start does.
[12]
The complete example
The manifest with every part from the steps, as the project uses it:
version: "1.0"
name: pipeline-watcher
model: openai:gpt-5-mini
instructions: |
You watch the company's data pipelines. On every run, call pipeline_status.
For each degraded pipeline that is not in open_alerts yet, call create_alert,
then save the new list of pipeline names with update_state("open_alerts", [...]).
Finish with one short line that says what you did.
identity:
provider: local
agent_id: pipeline-watcher
owner: data-platform
servers:
pipeline_tools:
type: stdio
command: python
args: ["pipeline_tools.py"]
triggers:
- type: cron
cron_expression: "*/15 * * * *"
cron_timezone: Europe/Zurich
- type: webhook
webhook_path: /pipeline-events
webhook_port: 9091
hmac_secret: ${PIPELINE_WEBHOOK_SECRET}
world:
open_alerts: []
context:
writable_keys: [open_alerts]
state_tool: true
conversation_max_messages: 40
journal:
level: full
path: .promptise/journal
secrets:
enabled: true
secrets:
pager_token: ${PAGER_TOKEN}
default_ttl: 3600
budget:
enabled: true
max_tool_calls_per_run: 10
max_runs_per_day: 200
on_exceeded: pause
health:
enabled: true
stuck_threshold: 3
on_anomaly: log
hooks:
- event: session_start
command: ["python", "hooks/log_event.py"]
- event: task_completed
command: ["python", "hooks/log_event.py"]
timeout: 5
config:
heartbeat_interval: 15
max_consecutive_failures: 5
restart_policy: on_failure
max_restarts: 3
max_invocation_time: 120
trigger_delivery:
max_retries: 1Run it:
cd pipeline-agents
cp .env.example .env # then set OPENAI_API_KEY, PIPELINE_WEBHOOK_SECRET, PAGER_TOKEN
promptise runtime validate agents/
promptise runtime start agents/pipeline-watcher.agent --detach
promptise runtime status
promptise runtime stopThe runtime CLI guide covers start, status, logs and stop in depth.
[13]
Running it in production
Review manifests like code
A manifest change can widen what an agent may do: a new server, a higher budget, open mode. Keep manifests in Git, review them in pull requests, and validate the directory in CI so a broken file never reaches a host. promptise runtime start agents/ stops at the first manifest that fails to load, so one bad file keeps every agent in the directory from starting.
Keep secrets out of the file
Put credentials with ${VAR} references either in the server spec that uses them (bearer_token, headers), where the agent never sees them, or in secrets.secrets. Secret-scope values stay out of the file, the loaded config and the journal, and in open mode the agent reads them with the get_secret tool; a strict-mode process gets no such tool in 1.5.0. Don't commit .env; in containers, set variables in the environment instead. save_manifest() writes the loaded manifest, so references outside secrets.secrets are written with their resolved values.
Mind the working directory
Relative paths (stdio server arguments, hook commands, journal.path, memory.persist_directory) resolve against the directory where the runtime starts. Start it from the project root, or use absolute paths; a process that "forgot" its state usually found an empty journal elsewhere.
Load manifests on the host, deploy over the API with care
Manifests you start on the host may run commands: stdio servers and command hooks. The control API is stricter. A deploy over HTTP that contains a stdio server, a command hook or open-mode tool creation is refused with 403 COMMANDS_NOT_ALLOWED, and ${VAR} references in a deploy are refused with 403 ENV_REFERENCES_NOT_ALLOWED, unless the operator allows them. See the control API guide.
[14]
Common problems and fixes
Symptom | Cause | Fix |
|---|---|---|
| version: 1.0 without quotes is a number | Write version: "1.0" |
| A typo or a key from an older example | Use the key the hint names; old keys such as journal_level don't exist |
EnvVarNotFoundError with a traceback | A ${VAR} without a default is unset | Set it, add it to .env, or use ${VAR:-default} |
A literal ${...} in instructions fails to load | References resolve in every string, including the prompt | Rephrase the text; there is no escape syntax |
The process starts with old state | Manifests journal and recover by default | Set journal: {level: none} or recover_on_start: false |
| promptise validate is the .superagent command | Use promptise runtime validate for .agent files |
A hook script can't find a variable | Hook programs get a minimal environment | List it in env_passthrough or pass it in env |
A stdio server or hook script isn't found | A relative path resolved from another working directory | Start from the project root or use absolute paths |
[15]
Limitations
No Python objects. Callback hooks, guardrail scanners, approval policies, caches and a mission success_check must be set in code on a ProcessConfig.
One process per file. A manifest declares one process. To run several, put several files in a directory and start the directory; run multiple AI agents shows how.
No includes or inheritance. Shared settings are repeated in each file. Generate manifests with a template tool if you have many similar agents.
No escaping for `${`. A literal ${ in any string value is read as a reference.
Validation errors are verbose. They come from Pydantic and include the input and a documentation link; the first Value error line is the part to read.
[16]
Frequently asked questions
What is an AI agent manifest file?
It is a YAML file that declares one long-running agent process: model, instructions, tools, triggers, state, journal, governance limits, lifecycle hooks and process settings. A runtime reads it, checks it and starts the process. In Promptise the file has the .agent extension and runs with promptise runtime start.
What is the difference between a .agent and a .superagent file?
A .superagent file defines an agent you call yourself, from a chat, a script or a request. A .agent file defines a process that runs on its own: it adds triggers, a journal, governance and hooks. Use .superagent for request-and-response agents and .agent for agents that wake on schedules or events.
How do I validate an AI agent YAML file before deploying it?
Run promptise runtime validate agents/ in CI. It checks every manifest against the schema, rejects unknown keys with a hint, and exits with 1 when any file fails. Add a test that calls manifest_to_process_config(load_manifest(path)) to build each config completely, as the runtime does at start.
How do I keep API keys out of an agent manifest?
Reference them as ${VAR}. A key the agent's tools need goes in the MCP server spec, for example bearer_token: ${TICKETS_TOKEN}, so the model never sees it. Values under secrets.secrets stay unresolved until the process starts and are redacted from the journal; in open mode the agent reads them with the get_secret tool. Load the variables from the environment or a .env file that you don't commit.
Does an agent loaded from a manifest keep its state after a restart?
Yes, by default. A manifest without a journal section gets a checkpoint journal in .promptise/journal, and the process restores its context state, conversation and counters from the last checkpoint when it starts under the same name. Set recover_on_start: false to keep the journal but start fresh.
Can I put Python callbacks in a manifest?
No. A manifest hook runs an external command; a callback: key is an error. Register Python callbacks in code with process.hooks.on(...) or HookConfig(callback=...) on a ProcessConfig.
[17]
Next steps
Run and control the agent from a terminal: AI agent CLI.
Watch it live: AI agent monitoring dashboard in the terminal.
Learn what each hook event carries: AI agent lifecycle hooks.
Define request-and-response agents in YAML: AI agent YAML config files.
See the runtime's governance as a whole: autonomous AI agent runtime.
Reference: agent manifests, configuration and runtime hooks in the Promptise docs.