The Inbox

In The Display Stack you learned how tools show UI and collect user input within a single conversation turn. But what happens when something can't be resolved right now?

The inbox is a persistent async mailbox. An agent posts a request it can't fulfill immediately — and an external service resolves it later. The next time the agent runs, it picks up the result automatically. This works across sessions, server restarts, and different instances of the same agent.

When to use the inbox

The display stack handles synchronous, in-process interactions well:

The inbox handles asynchronous, cross-instance scenarios:

Think of it this way: the display stack is like handing someone a clipboard and waiting. The inbox is like dropping a letter in a mailbox and checking back later.

How it works

  1. The agent calls glove_post_to_inbox with a tag, a natural language request, and a blocking flag
  2. The item is persisted in the store with status "pending"
  3. An external service (background job, webhook, cron, admin action) resolves the item with a text response
  4. Next time agent.ask() runs, resolved items are injected into the conversation as text messages and marked "consumed"
  5. Pending blocking items appear as transient reminders so the agent knows it's still waiting

The inbox survives context compaction — pending items are preserved in the compaction summary so the agent never forgets what it's waiting for.

The built-in tool

When your store implements inbox methods, Glove automatically registers the glove_post_to_inbox tool. The agent can call it whenever it decides something needs to be tracked asynchronously.

glove_post_to_inbox — input schematypescript
{
  tag: string,       // Category label, e.g. "restock_watch", "payment_pending"
  request: string,   // Natural language: "Notify when Yirgacheffe is back in stock"
  blocking: boolean, // Default false. If true, agent should wait for resolution
}

The tool returns the inbox item ID. The agent can reference it in conversation and the user sees it tracked in the UI.

Blocking vs non-blocking

ModeBehaviorUse for
blocking: falseAgent continues normally, result arrives laterRestock watches, background jobs, optional notifications
blocking: trueAgent is told it cannot proceed until resolvedPayment confirmations, required approvals, critical dependencies

Blocking is soft enforcement — the agent receives a message saying it should wait, but it's not mechanically prevented from acting. This matches how tasks and permissions work in Glove.

Enabling the inbox

The inbox is enabled by implementing four optional methods on your store. All built-in stores (SqliteStore, MemoryStore, createRemoteStore) already support it.

StoreAdapter — inbox methodstypescript
interface StoreAdapter {
  // ...existing methods...

  // Inbox (optional — enables glove_post_to_inbox when present)
  getInboxItems?(): Promise<InboxItem[]>;
  addInboxItem?(item: InboxItem): Promise<void>;
  updateInboxItem?(
    itemId: string,
    updates: Partial<Pick<InboxItem, "status" | "response" | "resolved_at">>,
  ): Promise<void>;
  getResolvedInboxItems?(): Promise<InboxItem[]>;
}

When all four methods are present, Glove auto-registers the glove_post_to_inbox tool — just like glove_update_tasks is auto-registered when task methods exist.

The InboxItem type

glove-core/coretypescript
type InboxItemStatus = "pending" | "resolved" | "consumed";

interface InboxItem {
  id: string;               // Auto-generated unique ID
  tag: string;              // Category label
  request: string;          // What the agent asked for (natural language)
  response: string | null;  // External service's response (null while pending)
  status: InboxItemStatus;  // Lifecycle state
  blocking: boolean;        // Whether the agent should wait
  created_at: string;       // ISO 8601 timestamp
  resolved_at: string | null;
}

Both request and response are plain text. The agent writes in natural language, and the external service responds in natural language. No structured payloads — the model interprets the text.

Resolving items externally

The whole point of the inbox is that something outside the agent resolves the request. Glove provides a static helper for this:

Background job / webhook handlertypescript
import { SqliteStore } from "glove-sqlite";

// Resolve an inbox item from any process that has DB access
const resolved = SqliteStore.resolveInboxItem(
  "path/to/sessions.db",     // Same DB the agent uses
  "inbox_17119...",           // The item ID
  "Great news! The Yirgacheffe is back in stock and ready to order."
);

if (!resolved) {
  console.log("Item not found or already resolved");
}

This opens its own database connection, updates the item, and closes. It can be called from a completely separate process — a cron job, a webhook handler, an admin script, or another service entirely.

For web apps, you'll typically expose this as an API endpoint:

app/api/inbox/resolve/route.tstypescript
import { NextResponse } from "next/server";
import { SqliteStore } from "glove-sqlite";

export async function POST(req: Request) {
  const { itemId, response } = await req.json();

  const resolved = SqliteStore.resolveInboxItem(DB_PATH, itemId, response);

  if (!resolved) {
    return NextResponse.json({ error: "Not found or already resolved" }, { status: 404 });
  }
  return NextResponse.json({ ok: true });
}

React integration

The useGlove hook returns inbox alongside tasks:

app/components/chat.tsxtsx
const { inbox, tasks, timeline, sendMessage } = useGlove({ tools, sessionId });

// Show pending watches in a sidebar
{inbox.filter(i => i.status === "pending").map(item => (
  <div key={item.id}>
    <span className="tag">{item.tag}</span>
    <span className="request">{item.request}</span>
    <span className="status">pending</span>
  </div>
))}

// Resolved items show up too — until consumed by the agent
{inbox.filter(i => i.status === "resolved").map(item => (
  <div key={item.id}>
    <span className="tag">{item.tag}</span>
    <span className="response">{item.response}</span>
    <span className="status">resolved</span>
  </div>
))}

Inbox state is hydrated from the store on mount and refreshed after each processRequest call.

Wiring remote store actions

When using createRemoteStore (the typical setup for Next.js apps), you need to wire inbox actions to your API routes. Without these, inbox falls back to in-memory storage and items vanish on reload.

app/lib/store-actions.tstypescript
import type { RemoteStoreActions } from "glove-react";

export const storeActions: RemoteStoreActions = {
  // ...existing getMessages, appendMessages...

  // Inbox — required for persistence across reloads
  getInboxItems: (sid) =>
    fetch(`/api/sessions/${sid}/inbox`).then(r => r.json()),

  addInboxItem: (sid, item) =>
    fetch(`/api/sessions/${sid}/inbox`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ item }),
    }),

  updateInboxItem: (sid, itemId, updates) =>
    fetch(`/api/sessions/${sid}/inbox/update`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ itemId, updates }),
    }),

  getResolvedInboxItems: (sid) =>
    fetch(`/api/sessions/${sid}/inbox/resolved`).then(r => r.json()),
};

The corresponding API routes delegate to SqliteStore methods — the same pattern as the message routes.

System prompt guidance

Like all tools, the agent uses the inbox better when you explain it in the system prompt. Here's what works well:

System prompt excerpttext
## Async Notifications
- Some requests can't be fulfilled immediately (out of stock, pending approval, etc.)
- Use glove_post_to_inbox to track these for the customer
- Use a descriptive tag (e.g. "restock_watch", "approval_pending")
- Describe what the customer wants in natural language in the request field
- Set blocking=false unless the customer literally cannot proceed without the result
- When an inbox item is resolved, you'll see the response — inform the customer

Example: Coffee shop restock

The Coffee Shop example demonstrates the inbox with inventory tracking:

  1. Customer asks for Yirgacheffe — it's out of stock
  2. Agent offers to watch for restocking and calls glove_post_to_inbox with tag "restock_watch"
  3. The item appears in the "Watching" section of the sidebar
  4. An external process resolves the item (simulated via POST /api/inbox/simulate-restock)
  5. Customer sends a new message — the agent picks up the resolved item and says "Great news! The Yirgacheffe is back in stock"

Lifecycle diagram

Inbox item lifecycletext
Agent calls glove_post_to_inbox


┌─────────┐   External service    ┌──────────┐   Next ask() call   ┌──────────┐
│ pending  │ ──────────────────▶  │ resolved │ ─────────────────▶  │ consumed │
└─────────┘   resolveInboxItem()  └──────────┘   injected as text  └──────────┘
    │                                                                     │
    │ (if blocking)                                                       │
    ▼                                                                     ▼
Agent sees transient                                          Agent sees resolved
"still pending" reminder                                      response in context
on each ask() call                                            and informs the user

Comparison with the display stack

FeatureDisplay StackInbox
TimingSynchronous — within a single turnAsynchronous — across sessions and instances
Resolved byThe user (clicking buttons, filling forms)External services (background jobs, webhooks)
PersistenceEphemeral — slots live in memoryPersistent — survives restarts and reloads
UIRich React components via render/renderResultText-based — agent interprets the response
Use forForms, confirmations, data cardsRestock watches, payment webhooks, background jobs

Next steps