Contents · 15 sections↓
- 01How do you pause and resume an AI agent?
- 02What is an agent process lifecycle?
- 03When to suspend an agent (and when to stop it instead)
- 04What you need
- 05Step 1: Explore the state machine
- 06Step 2: Suspend a running process
- 07Step 3: Resume it and drain the queue
- 08Step 4: Suspend and resume over HTTP
- 09Step 5: Know what suspends a process by itself
- 10The complete example
- 11Running it in production
- 12Common problems and fixes
- 13Limitations
- 14Frequently asked questions
- 15Next steps
Sometimes you need a running AI agent to stop working for a while without shutting it down: an upstream system is in maintenance, a budget ran out, or someone wants to look at what it did before it does more. This guide explains how to pause and resume an AI agent safely. It covers the eight lifecycle states of a Promptise Foundry agent process, which transitions are allowed, how to suspend and resume from code and over HTTP, and what happens to triggers and queued events while the agent is paused.
How do you pause and resume an AI agent?
Suspend the process instead of stopping it. A suspended process keeps its model client, tools and triggers. Triggers still accept events and queue them, but no new run starts until you resume. A run that was already going finishes. In Promptise, suspend() and resume() move the process between the RUNNING and SUSPENDED states, and the state machine rejects anything else with a StateError:
await process.suspend() # RUNNING -> SUSPENDED; events queue up
print(process.status()["queue_size"])
await process.resume() # SUSPENDED -> RUNNING; the queue drainsKey takeaways:
Suspend pauses work, not the process. Nothing is torn down, and nothing is lost from the queue.
Every state change is checked. Eight states, a fixed table of allowed transitions, and a StateError for the rest.
Several things suspend a process for you: idle timeout, budget limits and health anomalies. Nothing resumes it automatically.
Events don't always run in arrival order after a pause. Don't rely on order across a suspension.
Listen to transitions for alerts and audit, because suspension emits no event of its own.
[02]
What is an agent process lifecycle?
A long-running agent is more than a model call. It holds connections to tools, listens to triggers, keeps a queue of work and runs in the background. A lifecycle is the set of states such a process can be in and the rules for moving between them. It answers questions an operator asks every day: is the agent working, paused, broken or gone, and what is it allowed to do next? A state machine that rejects invalid moves keeps the answers trustworthy. You can't "resume" an agent that crashed, and you can't start one twice.
In Promptise every AgentProcess owns a ProcessLifecycle. These are its states:
State | Meaning |
|---|---|
CREATED | Registered, never started. |
STARTING | Building the agent, connecting MCP servers, starting triggers. |
RUNNING | Accepting and processing trigger events. An idle agent waiting for its next event is RUNNING too. |
SUSPENDED | Paused. Triggers queue events; no new run starts. |
AWAITING | Reserved. Defined in the table below, but nothing in the 1.4.3 runtime enters it. |
STOPPING | Shutting down: cancelling runs, stopping triggers, closing connections. |
STOPPED | Shut down cleanly. Can be started again. |
FAILED | Too many failed runs, or a failed start. Can be started again. |
And the allowed transitions, printed from VALID_TRANSITIONS in step 1:
CREATED ──start()──▶ STARTING ──startup complete──▶ RUNNING
├──startup error──▶ FAILED
└──stop()──▶ STOPPING
RUNNING ──suspend(), idle_timeout, budget, health──▶ SUSPENDED ──resume()──▶ RUNNING
RUNNING ──(reserved, never entered in 1.4.3)──────▶ AWAITING ──resume()──▶ RUNNING
RUNNING, SUSPENDED, AWAITING ──too many failed runs──▶ FAILED
RUNNING, SUSPENDED, AWAITING ──stop(), max_lifetime──▶ STOPPING ──▶ STOPPED
STOPPED ──start()──▶ STARTING
FAILED ──start(), restart policy──▶ STARTINGEvery transition is recorded as a ProcessTransition with the old and new state, a UTC timestamp, a reason and optional metadata. The list is the process's history, and listeners you register are called after each transition.
What changes the state
Cause | Transition | Undone by |
|---|---|---|
suspend(), API POST .../suspend, dashboard suspend <name> | RUNNING → SUSPENDED | resume() |
idle_timeout with no completed run | RUNNING → SUSPENDED | resume() |
Budget on_exceeded="pause" or "escalate" | RUNNING → SUSPENDED | resume() |
Health on_anomaly="pause", or "escalate" with an escalation target | RUNNING → SUSPENDED | resume() |
max_consecutive_failures failed runs | → FAILED | start() or the restart policy |
stop(), budget "stop", mission complete |
| start() |
Restarts and failure handling have their own guide, AI agent auto-restart.
[03]
When to suspend an agent (and when to stop it instead)
Suspend when you expect to resume soon and want nothing lost:
A downstream system is in maintenance, and the agent's tools would fail or do damage.
You want to inspect what the agent did, or correct its instructions, before it acts again.
A budget or health check paused it, and a person decides whether it goes on.
Stop when the pause is long or open-ended:
A suspended process still holds its MCP connections, stdio server subprocesses and webhook ports. Stopping releases them.
Events keep piling up in memory while suspended, up to 1,000 per process. Stopping closes webhook ports, so senders get an error and retry later instead of handing events to an agent that won't run them.
A deployment or config change is coming. stop() and start() rebuild everything from the new config.
Never use suspension to "fix" a failing agent. A FAILED process can't be resumed, only started again.
[04]
What you need
Python 3.10 or later and Promptise Foundry 1.4.3 or later.
No API key. The examples use the fake OpenAI-compatible model server from the auto-restart guide, flaky_llm.py, with a delay setting so runs take a known time.
About 15 minutes.
pip install "promptise>=1.4.3"[05]
Step 1: Explore the state machine
ProcessLifecycle works on its own, without an agent, which makes it easy to learn and to test. This script prints the transition table, registers three listeners, walks through a pause and tries one invalid move:
"""The process state machine on its own: transitions, StateError, listeners, snapshots."""
import asyncio
import json
from promptise.runtime import (
VALID_TRANSITIONS,
ProcessLifecycle,
ProcessState,
StateError,
)
print("valid transitions:")
for state, targets in VALID_TRANSITIONS.items():
print(f" {state.value:9} -> {', '.join(sorted(t.value for t in targets))}")
async def main() -> None:
lc = ProcessLifecycle() # starts in CREATED
def audit(t) -> None: # sync listener
print(f" audit: {t.from_state.value} -> {t.to_state.value} ({t.reason})")
async def alert_on_pause(t) -> None: # async listener
if t.to_state is ProcessState.SUSPENDED:
print(f" alert: paused because {t.reason!r}")
def broken(t) -> None: # a listener that raises is logged and skipped
raise RuntimeError("listener bug")
lc.add_listener(audit)
lc.add_listener(alert_on_pause)
lc.add_listener(broken)
print("\ntransitions:")
await lc.transition(ProcessState.STARTING, reason="start() called")
await lc.transition(ProcessState.RUNNING, reason="startup complete")
await lc.transition(ProcessState.SUSPENDED, reason="budget exceeded",
metadata={"limit": "max_cost_per_day"})
print(" can go to STARTING?", lc.can_transition(ProcessState.STARTING))
try:
await lc.transition(ProcessState.STARTING, reason="start() called")
except StateError as exc:
print(f" StateError: {exc}")
await lc.transition(ProcessState.RUNNING, reason="resume() called")
print("\nhistory:")
for t in lc.history:
print(f" {t.timestamp:%H:%M:%S.%f} {t.from_state.value:9} -> {t.to_state.value:9} {t.reason}")
snapshot = lc.snapshot()
restored = ProcessLifecycle.from_snapshot(json.loads(json.dumps(snapshot)))
print(f"\nrestored: {restored!r}, last metadata: {restored.history[2].metadata}")
asyncio.run(main())valid transitions:
created -> starting, stopped
starting -> failed, running, stopping
running -> awaiting, failed, stopping, suspended
suspended -> failed, running, stopping
awaiting -> failed, running, stopping
stopping -> failed, stopped
stopped -> starting
failed -> starting
transitions:
audit: created -> starting (start() called)
audit: starting -> running (startup complete)
audit: running -> suspended (budget exceeded)
alert: paused because 'budget exceeded'
can go to STARTING? False
StateError: Cannot transition from 'suspended' to 'starting'. Allowed targets: ['failed', 'running', 'stopping']
audit: suspended -> running (resume() called)
history:
18:03:36.606916 created -> starting start() called
18:03:36.608116 starting -> running startup complete
18:03:36.608258 running -> suspended budget exceeded
18:03:36.608369 suspended -> running resume() called
restored: ProcessLifecycle(state='running', transitions=4), last metadata: {'limit': 'max_cost_per_day'}What it shows:
`StateError` names the allowed targets. Starting a suspended process is invalid, and the message lists what is allowed instead. StateError is a ValueError. Check first with can_transition() when you are not sure.
Listeners can be sync or async. They run in registration order after the state has changed, so they can read the new state. The broken listener raised on every transition. The error was logged to stderr and skipped, and the transitions still happened. A listener can't veto a transition.
The history is complete and ordered, with the reason and metadata you passed. That is the audit trail of the process.
Snapshots round-trip through JSON. snapshot() returns the state and the full history as plain data, and from_snapshot() rebuilds a lifecycle from it.
AgentProcess registers its own listener first. It writes each transition to the journal when one is configured, and it emits process.failed and schedules restarts on FAILED. Your listeners run after it, through process.lifecycle.add_listener().
[06]
Step 2: Suspend a running process
Now suspend a real process. This one has a webhook trigger on port 9091 and a fake model that takes one second per call. The script sends one order, suspends the process while that order is running, and sends four more:
"""Suspend a running agent process, let events queue up, then resume it."""
import asyncio
import os
import shutil
import time
import aiohttp
os.environ["OPENAI_BASE_URL"] = "http://127.0.0.1:8765/v1" # fake model server
os.environ.setdefault("OPENAI_API_KEY", "sk-fake")
from flaky_llm import FlakyLLM
from promptise.runtime import (
AgentProcess,
JournalConfig,
ProcessConfig,
StateError,
TriggerConfig,
)
from promptise.runtime.journal import FileJournal
T0 = time.monotonic()
URL = "http://127.0.0.1:9091/orders"
def log(message: str) -> None:
print(f"{time.monotonic() - T0:5.1f}s {message}")
def show(process: AgentProcess) -> None:
s = process.status()
log(f"state={s['state']} invocations={s['invocation_count']} queue={s['queue_size']}")
async def post(session: aiohttp.ClientSession, order: int) -> None:
async with session.post(URL, json={"order": order}) as resp:
log(f"POST order {order} -> HTTP {resp.status}")
async def main() -> None:
shutil.rmtree("journal", ignore_errors=True)
llm = FlakyLLM()
llm.delay = 1.0 # each model call takes one second
await llm.start()
process = AgentProcess("orders", ProcessConfig(
model="openai:gpt-5-mini",
instructions="Check each new order.",
triggers=[TriggerConfig(type="webhook", webhook_path="/orders", webhook_port=9091)],
journal=JournalConfig(level="full", path="journal"),
))
await process.start()
async with aiohttp.ClientSession() as session:
await post(session, 1)
await asyncio.sleep(0.3) # order 1 is now running
await process.suspend()
log("suspended while order 1 runs")
for order in (2, 3, 4, 5):
await post(session, order)
await asyncio.sleep(2)
show(process) # order 1 finished, 2-5 wait
try:
await process.suspend()
except StateError as exc:
log(f"suspend again: {exc}")
await process.resume()
log("resumed")
await asyncio.sleep(5)
show(process)
entries = await FileJournal("journal").read("orders", entry_type="trigger_event")
log(f"processing order: {[e.data['payload']['order'] for e in entries]}")
states = await FileJournal("journal").read("orders", entry_type="state_transition")
log(f"journaled transitions: {[e.data['to_state'] for e in states]}")
await process.stop()
await llm.stop()
asyncio.run(main())The first half of the output:
0.3s POST order 1 -> HTTP 202
0.7s suspended while order 1 runs
0.7s POST order 2 -> HTTP 202
0.7s POST order 3 -> HTTP 202
0.7s POST order 4 -> HTTP 202
0.7s POST order 5 -> HTTP 202
2.7s state=suspended invocations=1 queue=4
2.7s suspend again: Cannot transition from 'suspended' to 'suspended'. Allowed targets: ['failed', 'running', 'stopping']Three things happened:
The run in progress finished. Order 1 had started before the suspend, and invocation_count reached 1 while the process was suspended. suspend() doesn't cancel runs. If you need the agent to stop mid-run, use stop().
The trigger kept accepting events. Orders 2 to 5 got HTTP 202, the webhook's "accepted" answer, and waited in the queue. Senders can't tell a suspended agent from a busy one. That is usually what you want during a short pause.
A second suspend is an error. SUSPENDED → SUSPENDED is not a transition. Code that pauses from several places, such as a budget and a person, should check process.state first or catch StateError.
[07]
Step 3: Resume it and drain the queue
The second half of the same run:
2.7s resumed
7.7s state=running invocations=5 queue=0
7.7s processing order: [1, 5, 2, 3, 4]
7.7s journaled transitions: ['starting', 'running', 'suspended', 'running']resume() moved the process back to RUNNING, and the four queued orders ran, one second each. The journal, at level="full", recorded each event as it was processed and each state change.
Look at the order: 1, 5, 2, 3, 4. While a process is suspended, its workers keep taking the next event off the queue, see that the process is paused, put the event back at the end of the queue, and check again half a second later. Each pass moves one event from the front to the back. After a pause of a few seconds the queue has rotated, so events run in a different order from the one they arrived in. Nothing is lost, but if order matters, put a sequence number or timestamp in the payload and let the agent or your tools check it. The trigger guides cover payload design.
[08]
Step 4: Suspend and resume over HTTP
In production, the person who pauses an agent is rarely in the same Python program. The runtime's control API exposes the same two methods. This script starts an AgentRuntime with one process, serves the API on localhost, and calls it:
"""Suspend and resume a process over the runtime's HTTP API."""
import asyncio
import json
import os
import aiohttp
os.environ["OPENAI_BASE_URL"] = "http://127.0.0.1:8765/v1" # fake model server
os.environ.setdefault("OPENAI_API_KEY", "sk-fake")
from flaky_llm import FlakyLLM
from promptise.runtime import AgentRuntime, OrchestrationAPI, ProcessConfig
BASE = "http://127.0.0.1:9100/api/v1/processes/orders"
async def call(session: aiohttp.ClientSession, method: str, path: str = "", full=False) -> None:
async with session.request(method, BASE + path) as resp:
body = await resp.json()
if method == "GET" and not full:
body = {"state": body["state"], "queue_size": body["queue_size"]}
print(f"{method} {path or '/'} -> {resp.status} {json.dumps(body, indent=2 if full else None)}")
async def main() -> None:
llm = FlakyLLM()
await llm.start()
async with AgentRuntime() as runtime:
await runtime.add_process("orders", ProcessConfig(
model="openai:gpt-5-mini", instructions="Check each new order.",
))
await runtime.start_all()
api = OrchestrationAPI(runtime, host="127.0.0.1", port=9100) # loopback: no token
await api.start()
async with aiohttp.ClientSession() as session:
await call(session, "POST", "/suspend")
await call(session, "GET", full=True) # the full status snapshot
await call(session, "POST", "/suspend") # already suspended
await call(session, "POST", "/resume")
await call(session, "GET")
await api.stop()
await llm.stop()
asyncio.run(main())POST /suspend -> 200 {"name": "orders", "suspended": true}
GET / -> 200 {
"name": "orders",
"process_id": "a8790ca1-1ff3-4ed7-82b2-71e5e183c950",
"state": "suspended",
"execution_mode": "strict",
"invocation_count": 0,
"consecutive_failures": 0,
"restart_count": 0,
"journal_enabled": false,
"trigger_count": 0,
"dynamic_trigger_count": 0,
"custom_tool_count": 0,
"rebuild_count": 0,
"spawned_process_count": 0,
"conversation_messages": 0,
"has_memory": false,
"queue_size": 0,
"dead_letter_count": 0,
"filtered_count": 0,
"pending_retries": 0,
"uptime_seconds": 0.0021297499997672276
}
POST /suspend -> 400 {"error": {"code": "SUSPEND_FAILED", "message": "Suspend failed: Cannot transition from 'suspended' to 'suspended'. Allowed targets: ['failed', 'running', 'stopping']"}}
POST /resume -> 200 {"name": "orders", "resumed": true}
GET / -> 200 {"state": "running", "queue_size": 0}POST /api/v1/processes/{name}/suspend and .../resume call suspend() and resume(), and a StateError comes back as HTTP 400 with the code SUSPEND_FAILED or RESUME_FAILED and the same message. GET /api/v1/processes/{name} returns the full status snapshot of the process.
On localhost the API needs no token. Bound to any other address it refuses to start without auth_token, and every route except the health check requires it. The AI agent control API covers authentication, the Python client and every route.
Read a status snapshot
status() (and the GET route) returns a plain dict. The fields that matter for pausing:
Field | Use |
|---|---|
state | The lifecycle state as a string. |
queue_size | Events waiting. Grows while suspended. |
invocation_count | Completed runs. Frozen while suspended, except for a run that was in flight. |
dead_letter_count | Events that won't run. Should not grow while suspended. |
uptime_seconds | Time since the last start, including time spent suspended. |
consecutive_failures, restart_count | Failure streak and restart streak. |
The snapshot has no "suspended since" time and no reason. Read both from process.lifecycle.history[-1], or from the journal's state_transition entries over the API.
[09]
Step 5: Know what suspends a process by itself
You are not the only one who suspends processes. Budget limits (on_exceeded="pause"), health anomalies (on_anomaly="pause" or "escalate") and idle_timeout do it too, and none of them resumes the process again. AI agent cost control and AI agent health monitoring cover the first two. idle_timeout needs a closer look:
"""idle_timeout suspends a quiet process; a resume can be undone by the next heartbeat."""
import asyncio
import os
import time
os.environ["OPENAI_BASE_URL"] = "http://127.0.0.1:8765/v1" # fake model server
os.environ.setdefault("OPENAI_API_KEY", "sk-fake")
from flaky_llm import FlakyLLM
from promptise.runtime import AgentProcess, ProcessConfig, TriggerEvent
T0 = time.monotonic()
def log(message: str) -> None:
print(f"{time.monotonic() - T0:5.1f}s {message}")
async def main() -> None:
llm = FlakyLLM()
llm.delay = 1.5 # a model call takes 1.5 seconds
await llm.start()
process = AgentProcess("digest", ProcessConfig(
model="openai:gpt-5-mini",
instructions="Summarize each event.",
idle_timeout=2.0, # suspend after 2 s without a completed run
heartbeat_interval=1.0, # the idle check runs on every heartbeat
))
process.lifecycle.add_listener(
lambda t: log(f"{t.from_state.value} -> {t.to_state.value} ({t.reason})")
)
await process.start()
await asyncio.sleep(2.6)
for n in range(3):
await process.inject(TriggerEvent(trigger_id="manual", trigger_type="manual",
payload={"n": n}))
await process.resume()
await asyncio.sleep(4)
s = process.status()
log(f"state={s['state']} invocations={s['invocation_count']} queue={s['queue_size']}")
await process.stop()
await llm.stop()
asyncio.run(main()) 0.0s created -> starting (start() called)
0.3s starting -> running (startup complete)
2.3s running -> suspended (idle timeout (2s))
2.9s suspended -> running (resume() called)
3.3s running -> suspended (idle timeout (3s))
6.9s state=suspended invocations=1 queue=2
6.9s suspended -> stopping (stop() called)
6.9s stopping -> stopped (shutdown complete)The process was suspended 2 s after startup because no run had completed. Three events were injected, and resume() moved it back to RUNNING. But the idle clock measures time since the last *completed* run, and resuming doesn't reset it. At the next heartbeat, 0.4 s later, the first run was still in progress, so the process was idle by that measure and was suspended again. One event ran; two stayed queued.
So in 1.4.3, a process suspended by idle_timeout and then resumed can be suspended again at its next heartbeat, unless a run completes before the heartbeat comes round. With real model calls that take several seconds, that is likely. Also, a process whose trigger fires less often than its idle timeout suspends itself between triggers and stops running. Keep idle_timeout=0, the default, unless you have a reason and a resume plan for it.
[10]
The complete example
pause-resume/
├── flaky_llm.py # fake OpenAI-compatible model with a delay setting
├── state_machine.py # ProcessLifecycle, StateError, listeners, snapshots (step 1)
├── suspend_demo.py # suspend mid-run, queue events, resume (steps 2-3)
├── api_demo.py # suspend and resume over the control API (step 4)
└── idle_demo.py # idle_timeout suspension and re-suspension (step 5)pip install "promptise>=1.4.3"
python state_machine.py
python suspend_demo.py # uses ports 8765 and 9091
python api_demo.py # uses ports 8765 and 9100
python idle_demo.pyTo run against a real model, remove the two os.environ lines and the FlakyLLM calls, and set OPENAI_API_KEY.
[11]
Running it in production
Alert when a process is suspended
The runtime emits process.started, process.stopped, process.failed and process.restarted, but no event for suspension. A budget that pauses an agent at 3 a.m. is invisible until someone checks. Add a listener that turns suspension into an event your sinks deliver:
from promptise.events import emit_event
from promptise.runtime import ProcessState
def on_transition(t):
if t.to_state is ProcessState.SUSPENDED:
emit_event(notifier, "process.suspended", "warning",
{"process_name": process.name, "reason": t.reason},
agent_id=process.name)
process.lifecycle.add_listener(on_transition)A WebhookSink(url, events=["process.suspended"]) on the same notifier then posts it to Slack. See AI agent notifications.
Keep the history
lifecycle.history lives in memory and is gone when the program exits. With JournalConfig(level="checkpoint") or higher, every transition is also written to the journal as a state_transition entry with its reason, which survives restarts and is readable over the API. Durable AI agents explains the journal.
Bound the pause
A suspended process queues events in memory, up to 1,000. When the queue is full, a webhook trigger answers 503 and other triggers wait until there is room. For pauses longer than a few minutes, stop() the process: its webhook port closes and senders retry later, instead of filling a queue that may be lost. If a person must decide whether a paused agent goes on, record who paused it and why in the reason, and review paused processes on a schedule.
Don't build on AWAITING
AWAITING is in the state enum and the transition table, and resume() accepts it, but no code in 1.4.3 puts a process into it. An agent waiting for its next trigger reports RUNNING. Treat AWAITING as reserved; dashboards that count "waiting" agents should count RUNNING processes with an empty queue.
[12]
Common problems and fixes
Symptom | Cause | Fix |
|---|---|---|
| Suspending twice, often a budget and a person at once | Check process.state first, or catch StateError |
resume() raises on a broken agent | A FAILED process can't be resumed | stop(), then start(), after fixing the cause |
stop() raises StateError on a process | It was never started (CREATED), and CREATED → STOPPING isn't allowed | Skip stop() for CREATED processes, as AgentRuntime.stop_all() does |
A suspended process still finished one run | suspend() doesn't cancel a run in progress | Use stop() to end runs immediately |
Events run out of order after a resume | Paused workers move events to the back of the queue | Don't rely on order; put a sequence number in the payload |
A resumed process is suspended again within seconds | idle_timeout counts from the last completed run, and resume doesn't reset it | Set idle_timeout=0 |
A process paused itself and nobody noticed | Suspension emits no event | Add a listener that emits process.suspended |
The API answers 400 SUSPEND_FAILED | The process isn't RUNNING | Read state from GET /api/v1/processes/{name} first |
[13]
Limitations
No timed pause. There is no "suspend for 30 minutes". Schedule the resume() yourself, or use a cron job that calls the API.
No automatic resume. Budget, health and idle suspensions stay until something calls resume(). Daily budget resets don't resume a process.
Suspension is in memory. If the program restarts, every process starts fresh in RUNNING, and queued events are gone. The journal keeps the record, not the queue.
No per-trigger pause. Suspending pauses all of a process's triggers at once. To pause one source, remove or filter that trigger.
`AWAITING` is unused in 1.4.3, as described above.
[14]
Frequently asked questions
What is the difference between suspending and stopping an AI agent?
Suspending pauses work but keeps everything set up: the model client, MCP connections and triggers stay, and new events wait in a queue. Stopping tears all of that down: runs in progress are cancelled, triggers stop, and webhook ports close. Suspend for short pauses you will resume, and stop for long ones or before a config change.
Does a paused AI agent lose events?
Not while the program keeps running. Triggers accept events during a suspension, and they wait in the process queue until you resume. They are held in memory, though, so they are lost if the program exits, and they may not run in arrival order after a long pause.
Can I pause an AI agent in the middle of a task?
suspend() stops new runs from starting, but a run that has already begun finishes. To end a run immediately, stop the process. For a pause inside a run, before a specific tool call, use human approval on that tool instead of suspension.
What does StateError mean?
It means the code tried a transition the lifecycle doesn't allow from the current state, such as suspending a process that is already suspended, or resuming one that failed. The message names the current state, the target and the allowed targets. Check with can_transition() or read process.state first.
What are the states of an AI agent process?
Promptise processes have eight: CREATED, STARTING, RUNNING, SUSPENDED, AWAITING, STOPPING, STOPPED and FAILED. A healthy agent waiting for work is RUNNING. SUSPENDED is a pause, FAILED means too many failed runs or a failed start, and AWAITING is reserved and not used in version 1.4.3.
How do I pause an AI agent from another service?
Run the control API next to the runtime and call POST /api/v1/processes/{name}/suspend, then .../resume later. Use a bearer token for anything but localhost. A state that doesn't allow the change returns HTTP 400 with the reason.
[15]
Next steps
Your first long-running agent, end to end: deploy AI agents in production.
What happens after FAILED: AI agent auto-restart.
Every route of the HTTP API: AI agent control API.
Tell a running agent what to do instead of pausing it: message a running AI agent.
The governance that pauses agents: the autonomous AI agent runtime.
Reference: process lifecycle and the control API in the Promptise docs.