> ## Documentation Index
> Fetch the complete documentation index at: https://praison.ai/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Bot Intentional Silence

> Let agents stay silent in group chats by returning NO_REPLY

<Note>
  Bot platform adapters now ship in the `praisonai-bot` package. `praisonai bot serve` still works exactly as documented here; for a standalone install see [praisonai-bot Migration](/docs/guides/praisonai-bot-migration).
</Note>

With `allow_silence: true`, an agent can return `NO_REPLY` (or a custom token) to send nothing — no message, no typing indicator, no error.

<Note>
  Both `NO_REPLY` (this page) and `BotLoopGuard` live in `praisonaiagents/bots/silence.py` alongside `classify_final()`, the classifier every adapter uses to distinguish silence from an empty reply. Where `NO_REPLY` lets an *agent* opt out of a single reply, [`BotLoopGuard`](/docs/features/bot-loop-protection) lets the *gateway* opt out of an entire runaway bot-to-bot exchange.
</Note>

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(name="group-bot", instructions="Reply in group chats only when helpful.")
agent.start("Should I respond to this off-topic message?")
```

The user posts in a group chat; with `allow_silence: true`, the agent can return `NO_REPLY` so nothing is sent.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph LR
    Msg[📨 Group message] --> Policy[group_policy gate]
    Policy --> Agent[🤖 Agent]
    Policy -->|observe → record passive, no run| Passive[📝 Passive context]
    Agent -->|NO_REPLY| Silent[🔇 No outbound message]
    Agent -->|Normal text| Reply[💬 Reply sent]

    classDef agent fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef tool fill:#189AB4,stroke:#7C90A0,color:#fff

    class Msg,Reply agent
    class Policy,Agent,Silent,Passive tool

```

<Note>
  Silence via `NO_REPLY` happens **after** the agent runs — the agent decides not to reply. The `observe` `group_policy` is different: it skips the agent-run branch entirely for unmentioned messages, but still writes them to the session as passive context. Use `NO_REPLY` when the agent should judge each message; use `observe` to retain group chatter without ever running the agent on it. See [`observe`: passive group context](/docs/docs/features/bot-gateway#observe-passive-group-context).
</Note>

## Quick Start

<Steps>
  <Step title="YAML">
    ```yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    channels:
      telegram:
        token: ${TELEGRAM_BOT_TOKEN}
        group_policy: respond_all
        allow_silence: true
    ```
  </Step>

  <Step title="Python">
    ```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
    from praisonai.bots import Bot
    from praisonaiagents import Agent

    agent = Agent(
        name="Group Helper",
        instructions="Reply only when asked a direct question. Otherwise return exactly NO_REPLY.",
    )
    bot = Bot("telegram", agent=agent, allow_silence=True)
    bot.run()
    ```
  </Step>
</Steps>

***

## How It Works

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant User
    participant Bot
    participant Agent

    User->>Bot: ambient group message
    Bot->>Agent: forward (after group_policy)
    Agent-->>Bot: NO_REPLY
    Note over Bot: allow_silence=true → send nothing
```

| Marker                       | Honoured when `allow_silence=true` |
| ---------------------------- | ---------------------------------- |
| `NO_REPLY`                   | ✅ default token                    |
| `[SILENT]`                   | ✅                                  |
| `SILENT`                     | ✅                                  |
| Custom `silence_token`       | ✅ exact match only                 |
| Prose containing "NO\_REPLY" | ❌ not treated as silence           |

***

## Scheduled agents

An unattended monitor with nothing to report returns a silence marker, and the scheduled run skips delivery to chat while still recording as `succeeded`.

Scheduled and automation runs honour the same silence contract — a scheduled job whose whole output is `NO_REPLY`, `[SILENT]`, or `SILENT` skips delivery while the run still completes and is recorded as `succeeded` in history.

Intentional silence is a recorded non-outcome (`SUPPRESSED`), whereas a real delivery *failure* now flips the run to `on_failure` and bumps `undelivered_deliveries` — see [Scheduler Delivery → Delivery outcomes](/docs/features/scheduler-delivery#delivery-outcomes).

An intentional-silence run **also skips the continuable-session seed** — nothing was delivered, so nothing is claimed to have been. The next inbound message lands as a fresh turn, same as before. See [Scheduler Delivery → Continuable Delivery](/docs/docs/features/scheduler-delivery#continuable-delivery).

<Warning>
  **No opt-in flag on scheduled paths.** Ambient/group-chat silence requires `allow_silence: true`. Scheduled and automation delivery honour markers unconditionally — an unattended monitor spamming `NO_REPLY` into chat is never useful.
</Warning>

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

monitor = Agent(
    name="log-watcher",
    instructions=(
        "Check the error log summary. If there are new errors, list them. "
        "If the summary is empty, reply with exactly: NO_REPLY"
    ),
)
# Wire this agent into a scheduled job (see /docs/features/scheduled-run-policy).
# On empty-log runs, nothing lands in chat; job still shows 'succeeded' in history.
```

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
sequenceDiagram
    participant Scheduler
    participant Agent
    participant Silence as is_intentional_silence_response
    participant Delivery
    participant History

    Scheduler->>Agent: run due job
    Agent-->>Silence: output
    alt exact silence marker
        Silence-->>Delivery: skip
        Silence-->>History: record 'succeeded'
    else normal text
        Silence-->>Delivery: deliver
        Silence-->>History: record 'succeeded'
    end
```

| Marker                                       | Ambient / group chat (`allow_silence: true`) | Scheduled / automation delivery (always)                  |
| -------------------------------------------- | -------------------------------------------- | --------------------------------------------------------- |
| `NO_REPLY`                                   | ✅ default token                              | ✅                                                         |
| `[SILENT]`                                   | ✅                                            | ✅                                                         |
| `SILENT`                                     | ✅                                            | ✅                                                         |
| Whitespace-wrapped marker (`"  NO_REPLY  "`) | ✅                                            | ✅                                                         |
| Custom `silence_token` (ambient-only opt-in) | ✅ exact match                                | not applicable — scheduled path uses default markers only |
| Prose containing `"NO_REPLY"`                | ❌                                            | ❌                                                         |

<Note>
  Both the wrapper (`AgentScheduler`) and bot (`ScheduledAgentExecutor`) paths behave identically here — you don't need to know which one you're on.
</Note>

***

## Configuration

| Field           | Type   | Default | Description                                                        |
| --------------- | ------ | ------- | ------------------------------------------------------------------ |
| `allow_silence` | `bool` | `false` | Honour silence markers (opt-in)                                    |
| `silence_token` | `str`  | `None`  | Override marker; when set, only this exact string triggers silence |

Combine with `group_policy: respond_all` so the agent *may* respond, then chooses silence via `NO_REPLY`.

If you want the bot to *look at* every message but only *speak* when addressed, prefer [`group_policy: observe`](/docs/features/bot-observe-mode) (no agent runs on unmentioned messages, transcript still captures them) over `respond_all + NO_REPLY` (agent runs on every message, then chooses silence). Observe is cheaper (no LLM calls) and safer (no risk of accidental replies).

***

## Scheduled Runs

The same silence markers also suppress delivery on **scheduled** runs — `AgentScheduler`, `AsyncAgentScheduler`, and the full-gateway `ScheduledAgentExecutor` all honour `NO_REPLY` / `[SILENT]` / `SILENT`.

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent
from praisonai.scheduler import AgentScheduler

agent = Agent(
    name="Watchdog",
    instructions=(
        "Every hour, check for new alerts. "
        "If there are none, reply with exactly: NO_REPLY."
    ),
)
AgentScheduler(agent, task="Hourly check", deliver="telegram:123456").start("hourly")
```

<Note>
  On the **scheduled** path, silence handling is always on — there is no `allow_silence` toggle. An unattended monitor should never post the raw control token, so the marker unconditionally suppresses delivery. The run is still recorded as succeeded.
</Note>

As on the chat path, only an exact marker triggers silence — prose mentioning `NO_REPLY` is delivered normally.

See [Scheduler Delivery → Intentional Silence](/docs/docs/features/scheduler-delivery#intentional-silence) for the full flow.

***

## Visible-Outcome Guarantee for Empty Finals

Blank replies and `[tool_calls: …]` placeholders now get a recorded fallback instead of silently dropping.

`classify_final()` is the single decision point every adapter (Slack, Telegram, Discord, IRC, custom) uses to classify an agent's final reply. A deliberate `NO_REPLY` is still suppressed, but an *empty* final — blank, whitespace-only, or the machine `[tool_calls: …]` placeholder — now gets a visible fallback message so no adapter ends a turn with nothing shown.

```mermaid theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
graph TB
    A[Agent final] --> C{classify_final}
    C -->|silence| S[Suppress send]
    C -->|empty| F[Fallback message]
    C -->|text| D[Deliver as-is]

    classDef input fill:#6366F1,stroke:#7C90A0,color:#fff
    classDef decide fill:#F59E0B,stroke:#7C90A0,color:#fff
    classDef suppress fill:#8B0000,stroke:#7C90A0,color:#fff
    classDef fallback fill:#189AB4,stroke:#7C90A0,color:#fff
    classDef deliver fill:#10B981,stroke:#7C90A0,color:#fff

    class A input
    class C decide
    class S suppress
    class F fallback
    class D deliver
```

| Final reply                               | `classify_final()` | Category           | Delivery                           |
| ----------------------------------------- | ------------------ | ------------------ | ---------------------------------- |
| `NO_REPLY`, `[SILENT]`, `SILENT`          | `"silence"`        | Deliberate silence | Suppressed — the no-reply contract |
| Blank, whitespace-only, `[tool_calls: …]` | `"empty"`          | Empty, not silence | Fallback message substituted       |
| Any real content                          | `"text"`           | Normal reply       | Delivered as-is                    |

### Override the fallback message

The default fallback is `"Task completed — no message to show."`. Override it via **either** an `empty_final_message` key on the existing `BotConfig.metadata` block (recommended — no new typed knob) or a direct `config.empty_final_message` attribute.

<CodeGroup>
  ```yaml agents.yaml theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  channels:
    slack:
      type: slack
      token: env:SLACK_BOT_TOKEN
      metadata:
        empty_final_message: "The agent completed the task but produced no reply text."
  ```

  ```python programmatic theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
  from praisonai.bots import BotConfig

  config = BotConfig(token="env:SLACK_BOT_TOKEN")
  config.empty_final_message = "Task done — nothing to display."
  ```
</CodeGroup>

Every fallback emission is logged at INFO level so the recorded non-outcome is operator-visible:

```
Empty-final resolution: substituted fallback for a blank/placeholder reply on <platform> (visible-outcome guarantee)
```

A tool-only run now still delivers a visible reply — the custom fallback if configured, the default otherwise:

```python theme={"theme":{"light":"vitesse-light","dark":"vitesse-dark"}}
from praisonaiagents import Agent

agent = Agent(
    instructions="Help the user with quick tasks.",
    # A run that only calls tools now still delivers a visible reply
    # (custom fallback if configured, default otherwise).
)
agent.start("Just log this event, no reply needed.")
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="Opt in explicitly">
    `allow_silence` defaults to `false` — existing bots behave unchanged.
  </Accordion>

  <Accordion title="Teach the agent the contract">
    Instructions should say when to return exactly `NO_REPLY` vs a normal reply.
  </Accordion>

  <Accordion title="Use for ambient group channels">
    Reduces noise when the bot listens to everything but should rarely speak.
  </Accordion>
</AccordionGroup>

***

## Related

<CardGroup cols={2}>
  <Card title="Scheduled Run Policy" icon="shield-halved" href="/docs/features/scheduled-run-policy">
    Guardrails and silent runs for unattended scheduled agents
  </Card>

  <Card title="Bot Loop Protection" icon="rotate" href="/docs/features/bot-loop-protection">
    Break runaway bot-to-bot reply loops
  </Card>

  <Card title="Gateway" icon="server" href="/docs/features/gateway">
    Channel configuration reference
  </Card>

  <Card title="Messaging Bots" icon="comments" href="/docs/features/messaging-bots">
    Multi-platform bot setup
  </Card>

  <Card title="Scheduler Delivery" icon="paper-plane" href="/docs/features/scheduler-delivery">
    Silence markers also suppress scheduled pushes
  </Card>

  <Card icon="triangle-exclamation" href="/docs/features/failure-reply">
    Failure-path counterpart of classify\_final — visible replies for failed turns
  </Card>
</CardGroup>
