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

Pause and Resume an AI Agent: Lifecycle States Explained

Pause 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.

Level
Intermediate
Reading time
16 min
Published
Oct 19, 2026
Last updated
Oct 19, 2026
Tested with
Promptise 1.4.3
By
Promptise Team
  • Agent Runtime
  • Deployment
  • Human-in-the-Loop
  • Observability
Contents · 15 sections↓
  1. 01How do you pause and resume an AI agent?
  2. 02What is an agent process lifecycle?
  3. 03When to suspend an agent (and when to stop it instead)
  4. 04What you need
  5. 05Step 1: Explore the state machine
  6. 06Step 2: Suspend a running process
  7. 07Step 3: Resume it and drain the queue
  8. 08Step 4: Suspend and resume over HTTP
  9. 09Step 5: Know what suspends a process by itself
  10. 10The complete example
  11. 11Running it in production
  12. 12Common problems and fixes
  13. 13Limitations
  14. 14Frequently asked questions
  15. 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.

[01]

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:

Python
await process.suspend()   # RUNNING -> SUSPENDED; events queue up
print(process.status()["queue_size"])
await process.resume()    # SUSPENDED -> RUNNING; the queue drains

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

Text
1
2
3
4
5
6
7
8
9
10
11
12
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──▶ STARTING

Every 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

→ STOPPING → STOPPED

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.

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

Pythonstate_machine.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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
"""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())
Output
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:

Pythonsuspend_demo.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
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
74
75
76
77
78
79
80
81
82
"""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:

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

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

Pythonapi_demo.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
39
40
41
42
43
44
45
"""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())
Output
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:

Pythonidle_demo.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
39
40
41
42
43
44
45
46
"""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())
Output
  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

Text
1
2
3
4
5
6
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)
>_Terminal
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.py

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

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

StateError: Cannot transition from 'suspended' to 'suspended'

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.

Contents

  1. 01How do you pause and resume an AI agent?
  2. 02What is an agent process lifecycle?
  3. 03When to suspend an agent (and when to stop it instead)
  4. 04What you need
  5. 05Step 1: Explore the state machine
  6. 06Step 2: Suspend a running process
  7. 07Step 3: Resume it and drain the queue
  8. 08Step 4: Suspend and resume over HTTP
  9. 09Step 5: Know what suspends a process by itself
  10. 10The complete example
  11. 11Running it in production
  12. 12Common problems and fixes
  13. 13Limitations
  14. 14Frequently asked questions
  15. 15Next 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 · 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.
  • // 02Intermediate · 17 minAI Agent State Between Runs: Context, Keys and AuditKeep AI agent state between runs: a key-value context with writable keys and an audit trail, how it differs from memory, and how to recover it on restart.
  • // 03Intermediate · 16 minDurable AI Agents: Crash Recovery and Safe ResumeHow to make an AI agent survive a crash: journal its progress, resume from the last step, and use idempotency keys so side effects never run twice.
  • // 04Intermediate · 16 minMessage a Running AI Agent: Inbox, Directives and AskSend instructions to a running AI agent without a restart: directives, context, corrections and questions, with TTLs, rate limits and safe scanning.
  • // 05Intermediate · 20 minAI Agent Control API: Manage Running Agents Over HTTPManage running AI agents over HTTP: deploy, stop and restart them, patch budgets and prompts, send messages, and secure the API. Real curl calls.
  • // 06Intermediate · 18 minAI Agent Health Monitoring: Catch Silent FailuresMonitor AI agent behavior in production: catch empty replies, rising error rates, stuck calls and loops, then log, pause or escalate. Real output.

Keep going.

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

All guidesLearning paths