Journal System¶
The journal is a process's durable, append-only record: state transitions, context writes, run results, errors, restarts, and a checkpoint of the recoverable state after every run. It powers three things: you can see what a process did (promptise runtime logs, GET /processes/{name}/journal), a process picks up where it left off after a crash or restart, and you can rewind a process to an earlier point.
from promptise.runtime import AgentProcess, JournalConfig, ProcessConfig
config = ProcessConfig(
model="openai:gpt-5-mini",
instructions="You write release notes.",
journal=JournalConfig(level="checkpoint", backend="file", path=".promptise/journal"),
)
process = AgentProcess("notes", config)
await process.start()
process.context.put("stage", "draft")
# ... trigger events run ...
await process.stop()
# Next time (a new Python process, after a crash or a deploy):
again = AgentProcess("notes", config)
await again.start()
print(again.recovered)
# {'lifecycle_state': 'stopped', 'entries_replayed': 1, 'context_keys': ['stage'],
# 'mission_restored': False, 'budget_restored': False,
# 'conversation_messages': 2, 'invocation_count': 1}
print(again.context.get("stage")) # 'draft'
The entries land in .promptise/journal/notes.jsonl, and the latest checkpoint in .promptise/journal/notes.checkpoint.json. Entries are filed under the process name, not its internal ID, so a restarted process (which gets a new process_id) and promptise runtime logs notes find them.
Turning it on¶
Journaling is off by default for a process: ProcessConfig.journal defaults to JournalConfig(level="none"). Pass a JournalConfig to turn it on; JournalConfig() itself defaults to level="checkpoint".
| Field | Type | Default | Description |
|---|---|---|---|
level |
"none" \| "checkpoint" \| "full" |
"checkpoint" |
How much is recorded (see below). "none" creates no journal at all |
backend |
"file" \| "memory" |
"file" |
FileJournal (JSONL on disk) or InMemoryJournal (lost when the Python process exits) |
path |
str |
".promptise/journal" |
Directory of the file backend, relative to the working directory |
recover_on_start |
bool |
True |
Restore the recorded state on the process object's first start() (see Recovery on start) |
In a manifest:
Unknown keys are ignored with a FutureWarning.
Levels¶
| Level | What the process records |
|---|---|
"none" |
Nothing (no journal object; rewind and recovery are unavailable) |
"checkpoint" |
State transitions, every context write (context_update), each run's result, run errors, restarts, recovery and rewind markers, operator actions from the API, secret accesses, mission evaluations, and a checkpoint after every run |
"full" |
Everything above, plus each trigger event, each run's start, and every tool call and tool result |
A failing journal backend is logged and never stops the process.
Entry types¶
Every record is a JournalEntry:
| Field | Type | Description |
|---|---|---|
entry_id |
str |
Unique ID (a UUID). This is what rewind targets |
process_id |
str |
The process name the entry is filed under |
timestamp |
datetime |
When it was recorded (UTC) |
entry_type |
str |
One of the types below |
data |
dict[str, Any] |
Type-specific payload |
What an AgentProcess writes:
entry_type |
Level | When | data |
|---|---|---|---|
state_transition |
checkpoint | Every lifecycle transition | from_state, to_state, reason, process_id |
context_update |
checkpoint | Every write to the context state (by your code, the API's PATCH /context, or the agent's state tool) |
key, value, timestamp, source |
invocation_result |
checkpoint | A run finished | invocation, event_id, trigger_type, duration_ms, response (first 2000 characters) |
checkpoint |
checkpoint | After every run, after a failed or refused run when a budget is enabled, when the mission's state changes, on stop(), after a rewind |
{"state": {...}}, see Checkpoints |
error |
checkpoint | A run raised | event_id, error_type, error (first 1000 characters) |
restart |
checkpoint | An automatic restart begins | reason, attempt (None for a max_lifetime recycle) |
restart_exhausted |
checkpoint | max_restarts reached |
reason, restarts, max_restarts, error |
recovered |
checkpoint | Journal recovery restored state at start | The same dict as process.recovered |
rewind |
checkpoint | A rewind was applied | target_entry_id, mode, actor, entries_affected, conversation_affected, code_affected, summary_note |
api_request |
checkpoint | A successful mutating request to the Orchestration API | Method, route, path parameters, status and client address (never the body) |
secret_access |
checkpoint | Secret scoping read, rotated, revoked or expired a secret | action ("access", "rotation", "revoke_all", "expired"), secret_name (never the value) |
mission_evaluation |
checkpoint | The mission judge evaluated progress | achieved, confidence, reasoning, progress_summary, mission_state, invocation_number, source |
trigger_event |
full | A run starts for a trigger event | trigger_id, trigger_type, event_id, payload |
invocation_start |
full | Before the model is called | invocation, event_id |
tool_call |
full | The agent called a tool | tool, args (redacted) |
tool_result |
full | A tool returned | tool, result (first 2000 characters). Results of get_secret are never journaled |
secret_access and mission_evaluation entries currently have an empty entry_id, so they can't be used as a rewind target; pick a neighboring entry.
Your own code can append any other type with journal.append(JournalEntry(...)). Entries serialize with entry.to_dict() and JournalEntry.from_dict(data).
Checkpoints¶
A checkpoint is a full snapshot of what a process can recover:
| Key | Content |
|---|---|
context_state |
The context key-value state |
lifecycle_state |
The process state when it was written |
invocation_count |
Runs so far |
conversation |
The conversation buffer (short-term memory) |
budget |
The budget counters (when a budget is enabled) |
mission |
The mission's state and evaluation history (when a mission is enabled) |
The context writes made before a checkpoint are journaled before it, so the journal's order matches what the checkpoint contains.
Recovery on start¶
With recover_on_start=True (the default), the first start() of a process object reads the journal for its name and restores what it recorded, before its own first transition is journaled:
- the context state: the last checkpoint's
context_stateplus thecontext_updateentries written after it (replayed withReplayEngine); recovered keys replace the same keys ofcontext.initial_state; - the mission (
MissionTracker.restore), when a mission is enabled; - the budget counters, when a budget is enabled, so a restart can't reset the daily limits;
- the conversation history, when
context.conversation_historyis on; - the invocation count.
Without a checkpoint (a crash before the first run ended), the context_update entries alone are replayed. A journal that holds neither restores nothing, and a journal that can't be read is logged and the process starts fresh.
process.recovered reports what was restored (or is None): lifecycle_state ("stopped" after a clean stop, "running" after a crash), entries_replayed, context_keys, mission_restored, budget_restored, conversation_messages and invocation_count. The same dict is journaled as a recovered entry and passed to SESSION_START hooks as recovered.
Not recovered from the journal: long-term memory (its provider persists it), secrets, queued trigger events and dead letters, and anything a tool did outside the process. Later starts of the same object (an automatic restart, restart_process) keep the live state and don't read the journal again. Set recover_on_start=False to always start fresh.
Secrets never reach the journal in plain text¶
When secret scoping is enabled, the process wraps its journal in a RedactingJournal. Every string in an entry's data and in a checkpoint passes through the secret scope's redactor before it reaches the backend, so tool arguments, run results and the conversation snapshot store [REDACTED] instead of a secret value, also after a rotation. get_secret results are never journaled at all.
To redact your own journal the same way:
from promptise.runtime.journal import FileJournal, RedactingJournal
journal = RedactingJournal(FileJournal(".promptise/journal"), scope.sanitize)
The redactor must return a redacted copy and leave its input unchanged. Values that were stored redacted come back redacted on recovery and rewind.
Reading entries¶
| From | How |
|---|---|
| The terminal | promptise runtime logs notes (newest entries, with their entry IDs; --follow for more), see CLI |
| The Orchestration API | GET /api/v1/processes/{name}/journal?limit=50&type=checkpoint; OrchestrationClient.get_journal(name, limit=, entry_type=) |
| Python | await journal.read(name, since=..., entry_type=..., limit=...) on a backend |
from datetime import datetime, timezone
from promptise.runtime.journal import FileJournal
journal = FileJournal(".promptise/journal")
entries = await journal.read("notes")
errors = await journal.read("notes", entry_type="error",
since=datetime(2026, 10, 1, tzinfo=timezone.utc))
checkpoint = await journal.last_checkpoint("notes")
Backends implement the JournalProvider protocol: append(entry), read(process_id, *, since, entry_type, limit), checkpoint(process_id, state), last_checkpoint(process_id) and close(). See Journal Backends.
Common pitfalls¶
- Expecting a journal by default. A
ProcessConfigwithoutjournal=writes nothing, sologs, recovery and rewind have nothing to work with. SetJournalConfig()(checkpoint level). - Two processes with the same name and path. Entries are filed under the name, so they share one file and recover each other's state. Give each process its own name or
path. - A relative
pathand a different working directory..promptise/journalis relative to where the process starts. A process started from another directory finds no journal and starts fresh; use an absolute path in services. backend: memoryand recovery. The in-memory journal disappears with the Python process, so nothing survives a crash. Use it for tests.recover_on_startand a fresh run. A test or a new deployment that should start clean must setrecover_on_start=Falseor use a new journal directory; otherwise it continues the old state.fulllevel and storage. Tool results and payloads can be large, and JSONL files are never rotated by Promptise. Rotate or archive them externally.
What's Next¶
- Journal Backends --
FileJournal,InMemoryJournalandRedactingJournal - Replay Engine -- how recovery rebuilds state from the journal
- Rewind -- roll a process back to an earlier entry
- Configuration --
JournalConfigamong the other options - CLI Commands --
promptise runtime logsandpromptise runtime rewind
Learn the concept
- Durable AI Agents: Crash Recovery and Safe Resume -- Journal progress and resume so side effects never run twice.
- AI Agent State Between Runs: Context, Keys and Audit -- What a process remembers between runs and how it is recorded.