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.
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.
glove_post_to_inbox with a tag, a natural language request, and a blocking flag"pending"agent.ask() runs, resolved items are injected into the conversation as text messages and marked "consumed"The inbox survives context compaction — pending items are preserved in the compaction summary so the agent never forgets what it's waiting for.
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.
{
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.
| Mode | Behavior | Use for |
|---|---|---|
blocking: false | Agent continues normally, result arrives later | Restock watches, background jobs, optional notifications |
blocking: true | Agent is told it cannot proceed until resolved | Payment 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.
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.
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.
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.
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".
// 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:
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 });
}The useGlove hook returns inbox alongside tasks:
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.
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.
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.
Like all tools, the agent uses the inbox better when you explain it in the system prompt. Here's what works well:
## 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 customerThe Coffee Shop example demonstrates the inbox with inventory tracking:
glove_post_to_inbox with tag "restock_watch"POST /api/inbox/simulate-restock)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| Feature | Display Stack | Inbox |
|---|---|---|
| Timing | Synchronous — within a single turn | Asynchronous — across sessions and instances |
| Resolved by | The user (clicking buttons, filling forms) | External services (background jobs, webhooks) |
| Persistence | Ephemeral — slots live in memory | Persistent — survives restarts and reloads |
| UI | Rich React components via render/renderResult | Text-based — agent interprets the response |
| Use for | Forms, confirmations, data cards | Restock watches, payment webhooks, background jobs |