PromptisePromptise
Docs
GitHub
Promptise - AI Framework LogoPromptise

The foundation layer for agentic intelligence. Build, secure and operate autonomous AI systems with Promptise Foundry.

pip install promptise

[01] Foundry

  • MCPcast
  • The Promptise Agent
  • Reasoning Engine
  • MCP
  • Agent Runtime
  • Prompt Engineering
  • Execution Engine
  • Agent Identity

[02] Resources

  • Documentation
  • GitHub
  • Guides
  • Learning Paths
  • Questions

[03] Company

  • About
  • Terms of Service
  • Privacy Policy
  • Cookie Policy
  • Subprocessors

© 2026 Promptise by Manser Ventures. All rights reserved.

Open source · Python

← Guides> Runtime, triggers and deployment

AI Agent Manifest: Declare a Long-Running Agent in YAML

Declare a long-running AI agent in a YAML manifest: tools, triggers, state, journal, budgets and hooks, with strict validation and real CLI output.

Level
Intermediate
Reading time
20 min
Published
Oct 21, 2026
Last updated
Oct 21, 2026
Tested with
Promptise 1.5.0
By
Promptise Team
  • Agent Runtime
  • Agents as Code (YAML)
  • Deployment
Contents · 17 sections↓
  1. 01How do you declare an AI agent in a YAML manifest?
  2. 02What is an AI agent manifest?
  3. 03When to use a .agent manifest (and when not to)
  4. 04What you need
  5. 05Step 1: Write a minimal manifest and validate it
  6. 06Step 2: Give the agent tools and triggers
  7. 07Step 3: Set state, memory and the journal
  8. 08Step 4: Add governance and process settings
  9. 09Step 5: Run commands on lifecycle events with hooks
  10. 10Step 6: Resolve environment variables with ${VAR}
  11. 11Step 7: Catch typos with strict validation
  12. 12The complete example
  13. 13Running it in production
  14. 14Common problems and fixes
  15. 15Limitations
  16. 16Frequently asked questions
  17. 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.

[01]

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.

YAML
1
2
3
4
5
6
7
8
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

Text
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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

JSON
{}

MCP servers (HTTP or stdio)

triggers

JSON
[]

What starts a run

world

JSON
{}

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

JSON
[]

External commands run on lifecycle events

config

JSON
{}

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.

>_Terminal
pip install "promptise>=1.5.0"

The example project, a pipeline watcher with one MCP tool server and one hook script:

Text
1
2
3
4
5
6
7
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:

Output
$ 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:

YAML
1
2
3
4
5
6
7
8
9
10
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:

Pythonpipeline_tools.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
"""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:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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.degraded

Each 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:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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: true

world 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:

YAML
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
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: 200

Each 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:

YAML
1
2
3
4
5
6
7
8
9
10
11
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: 3

Set 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:

YAML
1
2
3
4
5
6
hooks:
  - event: session_start
    command: ["python", "hooks/log_event.py"]
  - event: task_completed
    command: ["python", "hooks/log_event.py"]
    timeout: 5

The program gets the event as JSON on stdin. This one appends a line to hooks.log:

Pythonhooks/log_event.py
1
2
3
4
5
6
7
8
9
10
"""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:

Pythontry_manifest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
"""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())
Output
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:

Output
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:

Pythoninspect_manifest.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
"""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)
Output
$ 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:

Output
$ 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:

Output
$ 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)

Extra inputs are not permitted

A server entry

Extra inputs are not permitted; a misspelt command or url gives "needs 'url' (an HTTP server) or 'command' (a stdio server)"

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:

YAMLagents/pipeline-watcher.agent
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
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: 1

Run it:

>_Terminal
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 stop

The 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

Input should be '1.0'

version: 1.0 without quotes is a number

Write version: "1.0"

Text
unknown key(s) [...] (did you mean ...)

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

Invalid file extension. Expected .superagent

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.

Contents

  1. 01How do you declare an AI agent in a YAML manifest?
  2. 02What is an AI agent manifest?
  3. 03When to use a .agent manifest (and when not to)
  4. 04What you need
  5. 05Step 1: Write a minimal manifest and validate it
  6. 06Step 2: Give the agent tools and triggers
  7. 07Step 3: Set state, memory and the journal
  8. 08Step 4: Add governance and process settings
  9. 09Step 5: Run commands on lifecycle events with hooks
  10. 10Step 6: Resolve environment variables with ${VAR}
  11. 11Step 7: Catch typos with strict validation
  12. 12The complete example
  13. 13Running it in production
  14. 14Common problems and fixes
  15. 15Limitations
  16. 16Frequently asked questions
  17. 17Next steps

Learning paths

Want more structure? Paths put guides in order, like a short course.

See the paths →

> Runtime, triggers and deployment

Related guides

All guides in this topic →
  • // 01Intermediate · 20 minAI Agent CLI: Start, Stop and Inspect Running AgentsRun AI agents from the command line: start a runtime in the background, check status, follow logs, restart, stop and rewind. Real terminal output.
  • // 02Beginner · 15 minAI Agent Monitoring Dashboard in the TerminalMonitor AI agents in a terminal dashboard: process states, triggers, context state, logs and events, plus commands to suspend, resume and inject runs.
  • // 03Intermediate · 20 minMulti-Agent Event Bus: Chain Agents With EventsConnect AI agents with a pub/sub event bus: topics and wildcards, chaining agents on events and failures, full queues, and LangGraph and CrewAI compared.
  • // 04Advanced · 22 minDistributed AI Agents: Run Agents Across Many MachinesRun AI agents across multiple servers: a coordinator, node registration, health checks and remote control, and what to do when one node dies.
  • // 05Intermediate · 16 minPause and Resume an AI Agent: Lifecycle States ExplainedPause and resume a running AI agent: the eight lifecycle states, valid transitions, suspend and resume from code or HTTP, and what happens to queued work.
  • // 06Intermediate · 18 minAI Agent Auto-Restart: Restart Policies and TimeoutsAuto-restart an AI agent on failure: restart policies, backoff, restart limits, run timeouts and lifetime recycling, plus an alert when it gives up.

Keep going.

Browse every guide by topic and level, or follow a learning path that puts them in order.

All guidesLearning paths