Skip to main content
Different scope from the Run-State Journal. The inbound journal deduplicates and recovers webhook messages. The run-state journal persists the execution cursor of a single run. A webhook may spawn a run; the two journals track the two halves of the pipeline.
Inbound Journal tracks messages before agents process them, providing deduplication of webhook redeliveries and crash-safe replay of in-flight messages. The user sends a webhook message; the journal deduplicates redeliveries and tracks in-flight work.
On by default for gateway/bot runs — Every bot started via praisonai bot start, onboard, or bot.yaml gets the journal automatically. No code needed. The journal lives at ~/.praisonai/state/<platform>/ingress.sqlite. Set delivery.durable: false to opt out.

Automatic crash-replay

Every gateway-managed channel calls replay() for you at start-up and hot-reload — no startup hook needed. Every channel started via praisonai bot start, onboard, bot.yaml, or on hot-reload of a single channel automatically calls InboundJournal.replay() for its journal. What replay() does:
  • Resets stale claimed rows back to pending so they can be reprocessed via redelivery or session-resume (_resume_interrupted_turns(), PraisonAI #3379).
  • Quarantines genuine poison entries to the inbound DLQ under the shared age-gated policy (see Inbound DLQ).
  • Does not synthesise per-adapter native message objects to self-dispatch — that duplication is deliberately left to the session-resume path.
Best-effort: any failure is logged and skipped, so channel start never aborts. Reset counts are logged at INFO so operators can see the recovery happened.
The outbound reply path is now symmetric — see Outbound Resilience → Automatic crash-recovery on start.

Default behavior (no config needed)

Every bot started through build_session_manager (all shipped adapters) automatically gets an Inbound Journal at:
For example, a Telegram bot journals messages at ~/.praisonai/state/telegram/ingress.sqlite. A Discord bot uses ~/.praisonai/state/discord/ingress.sqlite. Each platform is fully isolated — no cross-platform dedup collisions. Set PRAISONAI_HOME to override the base directory:

Opt out or override path

Add a delivery: block to any channel in your bot.yaml or gateway.yaml:
Override the store directory:

Quick Start (advanced: manual setup)

Most users get the journal automatically. For custom setups outside build_session_manager:
1

Create journal that survives restarts

2

Enable on your bot

You don’t need to call complete() yourself when using BotSessionManager.chat() — successful chats mark the journal entry as completed automatically. You only need to call complete() manually if you’re driving InboundJournal directly without BotSessionManager.

How It Works

Before PR #1980, the default chat() path claimed entries but never called complete(). On startup, journal.replay() could re-issue messages the user had already received answers to. Upgrade to a release after 2026-06-19 — replay then only covers genuinely incomplete entries.

Automatic crash recovery under the gateway

The gateway calls InboundJournal.replay() for you — at channel start-up and on every hot-reload — so a gateway killed mid-turn recovers stranded messages without you invoking replay() yourself. replay() resets stale claimed rows back to pending so a redelivery — or the gateway’s session-level resume (Issue #3379) — can reprocess them exactly once, and quarantines genuine poison entries to the inbound DLQ. Reset counts are logged at INFO so operators can grep for recovery:

Gateway Restart Continuation

How in-flight turns resume after a gateway restart

Gateway Session Continuity

Session-level exactly-once resume path (#3379)

When to use which option


Configuration Options

Age-gated quarantine

replay() and the redelivery path no longer quarantine on attempts alone. An exhausted entry is only moved to the DLQ once it is both attempt-exhausted and at least dead_letter_min_age (default 6h) old — unless the failure is a known-permanent error class, which short-circuits immediately. A routine LLM outage that burns the claim budget in seconds keeps being retried instead of quarantining every in-flight message. See Durable Outbound Delivery → Age-gated dead-lettering for the shared policy and decision diagram. Introduced in PraisonAI PR #3521.

Per-platform examples


Common Patterns

Pattern 1: Dedup-only (webhook redelivery protection)

Pattern 2: Manual replay (only when driving InboundJournal outside the gateway)

Gateway-managed bots get startup and hot-reload replay for free (see Automatic crash-replay). This pattern is only for custom setups that drive InboundJournal directly.
After PR #1989, redelivery of a still-pending message also retries automatically — you no longer need to wait for a restart plus replay() to recover from a mid-flight crash if the platform redelivers the same message_id. replay() is still the right call on startup to recover any orphaned claims. Under the gateway, both mechanisms now compose: replay() unblocks stranded claimed rows at start-up, and redelivery + session-resume then reprocess them exactly once.

Pattern 3: Combining with InboundDLQ for full durability stack


Best Practices

The journal’s SQLite file must survive restarts for crash recovery to work. Use an absolute path or a location that persists across deployments.
Set claim_timeout to be longer than your p99 agent.chat() latency to avoid false stale entry detection.
When running under the gateway (praisonai bot start, onboard, bot.yaml, bot.WebSocketGateway), replay() is called automatically at channel start-up and hot-reload — you do not need to call it yourself. Reset counts are logged. See Automatic crash recovery under the gateway.
When you drive InboundJournal yourself (bare BotSessionManager, custom adapter loops), replay stale entries when your bot starts to recover from crashes.
The account parameter is part of the deduplication key. Keep it consistent for each bot instance.
Platforms redeliver webhooks at fixed intervals (Telegram retries within seconds, Slack waits ~3s before its first retry, etc.). Set claim_timeout shorter than your platform’s redelivery window so that a crashed worker’s claim becomes stale before the next redelivery arrives — that’s what enables automatic recovery without a restart.

Delivery Config

Full reference for the delivery: channel config block — defaults, opt-out, and path override

Inbound DLQ

Failure-side durability when agent execution fails

Durable Outbound Delivery

Outbound counterpart — persist outgoing messages with retry and idempotency

Bot Routing

Multi-channel session routing for complex bot setups

Visible-Outcome Guarantee

The delivery-side half of “no inbound message ends without a recorded outcome”
Automatic crash-replay introduced in PraisonAI PR #3622 (fixes #3621).