Imperal Docs
Guides

Confirmations

Confirmations are the chat flow that gates every write and destructive action: the user sees exactly what runs and the held call fires byte-identical on yes.

When a destructive (or chain) action would run, the web-kernel intercepts it, shows the user a confirmation card, and only executes after the user clicks "yes". This page explains the flow, the federal guarantees, and how your extension participates.

The user-facing flow

You: clear my done tasks

Webbee: "This will delete 3 completed tasks. Confirm?" — [✅ Yes] [✗ Cancel]

You: (clicks ✅)

Webbee: ✓ Cleared 3 completed task(s).

What happened end to end:

The platform picks tasks-mini.clear_done_tasks from the user's intent.

The platform sees action_type="destructive" on your tool, holds the call, and shows a ConfirmationCard in chat.

The Imperal Panel renders the card with [Yes] and [Cancel] buttons.

The user clicks Yes. The platform verifies the request came from the same user who triggered it.

The held call is then run with byte-identical arguments — exactly what the card showed. There is no re-planning and no second model run.

The action is recorded in the audit trail as user-accepted, retention federal_7y.

What you do

Just declare action_type="destructive":

@chat.function(
    "delete_folder",
    description="Permanently delete a folder and all its contents.",
    action_type="destructive",
    id_projection="folder_id",
)
async def delete_folder(ctx, params: DeleteFolderParams):
    await ctx.http.delete(f"/folders/{params.folder_id}")
    return {"text": "Folder deleted."}

That's it. The web-kernel handles the rest.

What the user sees

The confirmation card includes:

ElementSource
Title ("Delete folder?")i18n template by action_type
DescriptionYour tool's description (so write it well)
Function call entriesEach intercepted call rendered: tool name + key params
Action buttons[Yes, do it] / [Cancel]

Example for a chain (multiple destructive calls in one user turn):

Confirm 3 actions:
  1. delete_note(note_id="note_abc")
  2. delete_note(note_id="note_def")
  3. archive_thread(thread_id="thr_123")

[✅ Confirm all]   [✗ Cancel]

This means: all three run with those exact args, in that order, on accept. No re-planning, no model rerun.

The guarantees you can rely on

🔒

What you saw is what runs

The actions on the card run with byte-identical arguments — nothing is re-decided after you click Yes.

🚫

Acceptance carries no new data

Clicking Yes cannot inject new arguments. The actions are exactly the ones the card showed.

🆔

Same user, verified

An acceptance is only honored when it comes from the same user who triggered the action.

⏱️

No zombie acceptances

Confirmations expire alongside the chat session. An old, stale card cannot be accepted later.

🎚️

Consistent across plan shapes

Your confirmation preferences apply uniformly whether the action is a single step or part of a multi-step chain.

Multi-step chains and confirmations

When a user says "check my unread mail and delete the spam":

  1. The platform plans a 2-step sequence: [mail.list_unread → mail.delete_spam]
  2. Step 1 is read-only and runs immediately
  3. Step 2 is destructive — the platform pauses and shows a card with the actual count to be deleted
  4. The user accepts → step 2 runs with those exact, byte-identical arguments

The card shows real numbers ("Delete 7 spam threads?"), not placeholders ("Delete some threads?"). This is part of the Pre-Authorized Action Execution flow.

What you should NOT do

Panel button confirmation (a UI concern, not an SDK Call flag)

Confirmation gating in chat is driven by your tool's action_type ("write"/"destructive"), not by any flag on ui.Call. ui.Call(function, **params) only takes a function name plus call params — there is no confirm= argument; if you pass one, it is folded into the params, not treated as a confirmation directive.

For a panel button that should pop a quick "are you sure?" before firing, the confirmation lives on the panel action-wrapper dict as a "confirm" key (a Panel renderer prop), while the call itself stays a plain ui.Call:

from imperal_sdk import ui

@ext.panel("items", slot="left", title="Items")
async def items_sidebar(ctx, **kwargs):
    items = await ctx.store.query("items", limit=50)
    return ui.List(items=[
        ui.ListItem(
            id=str(item["id"]),
            title=item["name"],
            actions=[{
                "label": "Move to trash",
                "icon": "Trash2",
                "on_click": ui.Call("archive_item", item_id=item["id"]),
                "confirm": "Move this item to trash?",   # Panel renderer prop
            }],
        )
        for item in items.data
    ])

The "confirm" string is rendered by the Panel host as a confirm prompt on the button. It is independent of the chat confirmation flow above, which is decided entirely by action_type on the @chat.function.

Where to next

On this page