Outbound Ordering

Keep messages to the same chat delivered in the order they were sent, even under retries

Per-conversation FIFO ordering keeps a bot’s messages arriving in the same order the agent produced them β€” so users never see “part 2” before “part 1”.

Diagram

strict

msg 1
retrying

msg 2
held

πŸ’¬ User

best_effort (default)

msg 1
retrying

πŸ’¬ User

msg 2

Under retries, Retry-After back-off, rate limiting, or multi-worker drains, best_effort can deliver a later message to a chat before an earlier one that is still backing off. strict holds later same-lane messages until the head reaches sent or permanent_failure.

Quick Start

  1. Enable via the production preset

    The production reliability preset turns on strict per-conversation FIFO automatically:

    python
    from praisonaiagents import Agent
    from praisonai.bots import BotOS
    
    agent = Agent(name="assistant", instructions="Help the user.")
    bot = BotOS(agent=agent, platforms=["slack"], reliability="production")
    bot.start()
  2. Or opt in explicitly

    An explicit outbound_ordering always wins over the preset:

    python
    from praisonaiagents import Agent
    from praisonai.bots import BotOS
    
    agent = Agent(name="assistant", instructions="Help the user.")
    bot = BotOS(
        agent=agent,
        platforms=["slack"],
        reliability="production",
        outbound_ordering="strict",
    )
    bot.start()
  3. Group by a custom lane

    By default, all messages to the same channel/DM share a lane. Group by thread id, user id, or anything you want:

    python
    await outbox.enqueue(
        idempotency_key="msg-abc",
        target="slack:C0123456",
        payload={"text": "Hello!"},
        lane_key=f"slack:C0123456:thread:{thread_ts}",
    )

How It Works

Diagram
πŸ“± SlackπŸ’Ύ OutboundQueueπŸ€– AgentπŸ“± SlackπŸ’Ύ OutboundQueueπŸ€– Agentmsg 2 HELD (same lane)enqueue msg 1 (lane=chat-42)enqueue msg 2 (lane=chat-42)send msg 1429 Retry-After 5sretry msg 1 β†’ βœ…send msg 2 β†’ βœ…

In strict mode, only the earliest non-terminal entry of each lane is eligible to send. Later same-lane entries wait until the head reaches sent or permanent_failure. Different lanes still drain in parallel, so throughput is unaffected. In best_effort mode, pending entries drain in global wall-clock order with no per-lane gate.

ModeBehaviour
best_effortGlobal ts order, no per-lane gate. A later message can overtake an earlier one that is retrying.
strictPer-lane FIFO gate. Later same-lane messages held until the head is terminal. Lanes drain concurrently.

Choosing a Mode

Diagram

No, best throughput

Yes

No

Yes

What do I need?

Conversational coherence
under retries?

best_effort
default

Order per-thread
inside a channel?

strict

strict + custom lane_key

I want…Setting
Backward-compatible behaviour (best throughput, may reorder under retries)outbound_ordering="best_effort" (default)
Conversational coherence under retries / rate limits / multi-worker drainoutbound_ordering="strict" (production preset)
Per-thread ordering inside a shared channelstrict + custom lane_key

Configuration Options

Set on the outbox directly, via resolve_reliability, or via the reliability= preset.

OptionTypeDefaultDescription
ordering (on OutboundQueue)`”strict” \“best_effort”`"best_effort"Per-lane FIFO gate. strict holds later same-lane messages until the head reaches sent or permanent_failure.
lane_key (on enqueue)`str \None`None (= target)Custom conversation grouping. Defaults to target so messages to the same chat share a lane.
outbound_ordering (on resolve_reliability)`”strict” \“best_effort” \None`None (preset decides)Preset override. Explicit value always wins; unknown values raise ValueError.

Precedence (highest β†’ lowest):

  1. Explicit outbound_ordering= β€” e.g. "strict"
  2. reliability= preset β€” production β†’ strict, otherwise best_effort
  3. SDK default β€” best_effort

Best Practices

Use strict when message order is user-visible

Multi-part replies, streamed follow-ups, and “greeting then answer” flows all break if a later message overtakes an earlier one under retry. Turn on strict (or the production preset) for these.

Keep best_effort for independent, order-agnostic sends

Notifications, alerts, and one-off messages that do not depend on each other get the best throughput with best_effort. Strict adds a per-lane gate you do not need here.

Use a custom lane_key for per-thread ordering

Inside a shared channel, group by thread id so unrelated threads still drain in parallel while each thread stays ordered:

python
await outbox.enqueue(
    idempotency_key=f"reply-{msg_id}",
    target="slack:C0123456",
    payload={"text": reply},
    lane_key=f"slack:C0123456:thread:{thread_ts}",
)
Let explicit outbound_ordering override the preset

The production preset defaults to strict. Pass outbound_ordering="best_effort" explicitly if you deliberately want throughput over ordering on a production deployment β€” the explicit value wins.


Related