Everything your AI agents are waiting on you for, each with its risk: the one in focus open with exactly what it will do, approve or deny it from the keyboard (J/K, A, D) or several at once, and always allow a tool for an agent, for its details or for an hour. Decided rows fold away and the next opens, on motion.
Installation
When to use
- One place for everything your AI agents are waiting on a person for, across agents and tools.
- Products where agents act on real things (emails, refunds, records) and someone answers for what they do.
When not to use
- One request inside a conversation: Approval Card asks right where the assistant is working.
- Requests from people rather than agents (access, invites): Notifications center.
Usage
Approval inbox with your own data: every prop has the sample as its default, so pass only what you have.
"use client"
import { useRef, useState } from "react"
import { ApprovalInbox, type ApprovalRequest, type ApprovalInboxHandle } from "@/components/dashral/blocks/approval-inbox"
export default function Approvals() {
const inbox = useRef<ApprovalInboxHandle>(null)
// Kept in state: a new array replaces what the inbox shows, so don't build the list inline.
const [waiting] = useState<ApprovalRequest[]>([
{
id: "r-1",
agent: "Billing agent",
tool: "email.send",
title: "Send 3 payment reminders",
description: "Each customer gets the first reminder, with their invoice attached.",
details: [{ label: "To", value: "Acme, Globex, Initech" }],
risk: "low",
time: "2 min ago",
},
{ id: "r-2", agent: "Support agent", tool: "orders.refund", title: "Refund order #5821", risk: "high", destructive: true, time: "6 min ago" },
])
return (
<ApprovalInbox
ref={inbox}
requests={waiting}
history={[]}
rules={[]}
// Tell your agent runtime; while the promise is pending the button shows it, and a thrown Error keeps the request.
onDecide={async (request, decision, by) => console.log(request.id, decision, by)}
onRuleAdd={(rule) => console.log("always allow", rule.agent, rule.tool, rule.scope ?? "tool")}
onRuleRemove={(rule) => console.log("ask again", rule.agent, rule.tool)}
/>
)
// A request raised while the page is open: inbox.current?.add({ id, agent, tool, title, time: "Just now" })
}API reference
ApprovalInbox
An approval inbox for AI agents: every request waiting on you, the one in focus open and the rest one line each, each with its risk. Approve, deny or always allow it (for its tool, with the same details, or for an hour), move with J and K, decide with A and D (Enter approves), or tick several and decide them from the bar. A rule approves what it covers, one after another; anything that can't be undone always asks. Decided, folded under the queue, keeps what was approved or denied and by whom; Rules lists the rules, to remove. Your requests come in as requests, new ones through the handle's add, and every decision goes to onDecide.
| Prop | Type | Default |
|---|---|---|
title | React.ReactNode | "Approvals" |
requests | ApprovalRequest[]What's waiting, newest first. A new list (another array) replaces what's shown, so keep yours in state or outside the component rather than building it inline on every render. | sample |
history | ApprovalRecord[]What was decided before, newest first, for the Decided list, at first: decisions made here are added on top. | sample |
rules | ApprovalRule[]The rules your agents work under, at first: rules added and removed here change it. | sample |
onDecide | (request: ApprovalRequest, decision: ApprovalDecision, by: "You" | "Rule") => Promise<void> | voidA decision, by a button, a key or a new rule ( | — |
onRuleAdd | (rule: ApprovalRule) => Promise<void> | void | — |
onRuleRemove | (rule: ApprovalRule) => Promise<void> | void | — |
ref | React.Ref<ApprovalInboxHandle> | — |
className | string | — |
PlainMark
A tool without a brand, for your own requests' toolIcon: a plain icon on the muted ground, the same size as a mark.
| Prop | Type | Default |
|---|---|---|
icon | LucideIconRequired. | — |
ApprovalRiskBadge
A request's risk: a shield in its colour and its word; "High risk" where there's room.
| Prop | Type | Default |
|---|---|---|
risk | ApprovalRiskRequired. | — |
long | boolean | false |
ApprovalRequestRow
One request in the queue. Closed: the agent, the tool's mark, what it wants to do and its risk, on one line. Open: who asks with which tool and when, why, exactly what it will do, then Deny, Always allow (for this tool, with these details, or for an hour) and Approve. Opening and closing glide; on a phone the buttons go under the words.
| Prop | Type | Default |
|---|---|---|
request | ApprovalRequestRequired. | — |
expanded | booleanThe request in focus: open, with its details and buttons. Closed, it's one line you can open. | true |
onExpand | () => void | — |
selected | boolean | false |
onSelectedChange | (selected: boolean) => void | — |
busy | ApprovalDecision | nullThe decision being sent: its button shows it's working and the others wait. | null |
decided | ApprovalDecision | nullOnce it went through: "Approved" or "Denied" in place of its risk, for the beat before it folds away. | null |
onDecide | (decision: ApprovalDecision) => voidRequired. | — |
onAlwaysAllow | (scope: NonNullable<ApprovalRule["scope"]>) => voidOffers Always allow, with its scopes; left out, and for a request that can't be undone, it isn't offered. | — |
savingRule | booleanThe rule is being saved: Always allow shows it's working. | false |
type ApprovalDecision
= "approved" | "denied"type ApprovalRisk
= "low" | "medium" | "high"type ApprovalRequest
| Prop | Type | Default |
|---|---|---|
id | number | string | — |
agent | stringWhich agent asks, e.g. "Billing agent". | — |
tool | stringThe tool it wants to use, as your code names it, e.g. "email.send". A rule allows a tool for an agent. | — |
toolIcon | React.ReactNodeThe tool's mark (your integration's logo), beside the request. | — |
title | stringWhat it wants to do, as an action: "Send 3 payment reminders". | — |
description | stringWhy, or what will happen. | — |
details | { label: string; value: string }[]Exactly what it will do, as rows; shown when the request is open. | — |
risk | ApprovalRiskHow much harm a mistake would do. Left out: high when it can't be undone, otherwise none is shown. | — |
destructive | booleanCan't be undone: said so, approved in red, never covered by a rule, and A only takes you to its Approve. | — |
time | stringWhen it asked, e.g. "2 min ago". | — |
type ApprovalRecord
A request once decided, for the Decided list.
| Prop | Type | Default |
|---|---|---|
id | number | string | — |
agent | stringWhich agent asks, e.g. "Billing agent". | — |
tool | stringThe tool it wants to use, as your code names it, e.g. "email.send". A rule allows a tool for an agent. | — |
toolIcon | React.ReactNodeThe tool's mark (your integration's logo), beside the request. | — |
title | stringWhat it wants to do, as an action: "Send 3 payment reminders". | — |
description | stringWhy, or what will happen. | — |
details | { label: string; value: string }[]Exactly what it will do, as rows; shown when the request is open. | — |
risk | ApprovalRiskHow much harm a mistake would do. Left out: high when it can't be undone, otherwise none is shown. | — |
destructive | booleanCan't be undone: said so, approved in red, never covered by a rule, and A only takes you to its Approve. | — |
time | stringWhen it asked, e.g. "2 min ago". | — |
decision | ApprovalDecision | — |
by | stringWho decided: a person's name, "You", or "Rule" for one a rule let through. | — |
decidedAt | stringWhen it was decided, e.g. "Yesterday". | — |
type ApprovalRule
An agent that may use a tool without asking. scope narrows it: "tool" (the default) for any use, "details" only with the same details as the request it came from (kept in details), "hour" until until (epoch milliseconds).
| Prop | Type | Default |
|---|---|---|
agent | string | — |
tool | string | — |
scope | "tool" | "details" | "hour" | — |
details | string | — |
until | number | — |
type ApprovalInboxHandle
Brings in requests raised while the page is open.
| Prop | Type | Default |
|---|---|---|
add | (request: ApprovalRequest) => voidA new request: it grows in at the top of the queue, or goes straight to Decided when a rule covers it. | — |