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. The built-in MemoryStore from glove-core implements them out of the box, and createRemoteStore from glove-react exposes them on its RemoteStoreActions contract — wire those to your backend to persist the inbox across sessions.

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. Resolution is a plain store update: flip the item's status to "resolved", attach the response text, and stamp resolved_at. The next time processRequest runs, the agent picks the item up via getResolvedInboxItems(), injects it into context, and marks it "consumed".

Background job / webhook handlertypescript
// Any process that shares the store backend — cron, webhook, admin script —
// just calls updateInboxItem on a StoreAdapter pointed at the same session.
async function resolveInboxItem(
  store: StoreAdapter,
  itemId: string,
  response: string,
) {
  if (!store.updateInboxItem) {
    throw new Error("Store does not support inbox items");
  }
  await store.updateInboxItem(itemId, {
    status: "resolved",
    response,
    resolved_at: new Date().toISOString(),
  });
}

For web apps, expose this as an API endpoint that builds a store adapter for the target session and updates the item:

app/api/inbox/resolve/route.tstypescript
import { NextResponse } from "next/server";
import { resolveInboxItem } from "@/lib/inbox";
import { storeForSession } from "@/lib/store";

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

  const store = storeForSession(sessionId);
  await resolveInboxItem(store, itemId, response);

  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 your store backend — 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