Skip to content

Write Actions & Confirmation Gate

The assistant uses a two-step confirmation gate for every tool that mutates data. The AI previews what it will do, the user confirms or cancels, and only then does the mutation execute.

Every write tool accepts a confirmed boolean parameter (defaults to false):

  1. Preview — the AI calls the tool with confirmed: false. The tool validates inputs, resolves references (tags, phone numbers), checks for duplicates, and returns a JSON preview with "status": "preview".
  2. User decision — the UI renders Confirm, Edit, and Cancel buttons below the preview card.
  3. Execute — if the user clicks Confirm, the AI calls the tool again with confirmed: true. The tool performs the mutation and returns "status": "created" (or "updated", "sent", etc.) with a deep_link to the new or modified entity.
AI calls tool(confirmed: false)
→ Tool validates + returns preview JSON
→ UI shows ToolCallCard (yellow/pending) + action buttons
→ User clicks Confirm
→ AI calls tool(confirmed: true)
→ Tool executes mutation + returns success JSON with deep_link
→ UI shows ToolCallCard (green/success) + "Open" button

All write tools follow the same confirmation pattern:

ToolActionDomain
create_contactCreate a new contact with phone, tagsContacts
update_contactUpdate contact fieldsContacts
add_contact_noteAdd a note to a contactContacts
manage_contact_tagsAdd/remove tags (auto-creates missing tags)Contacts
manage_contact_emailsAdd/remove email addressesContacts
manage_contact_phonesAdd/remove phone numbersContacts
send_smsSend an SMS (shows recent conversation in preview)Messages
create_taskCreate a task for a contactTasks
update_taskUpdate task fieldsTasks
complete_taskMark a task completeTasks
dismiss_taskDismiss a taskTasks
reopen_taskReopen a completed/dismissed taskTasks
create_planApply a plan template to a contactWorkflow
create_plan_templateCreate a new plan templateWorkflow
update_plan_templateUpdate template metadataWorkflow
create_agentCreate a voice agentInfrastructure
update_agentUpdate voice agent settingsInfrastructure
create_analyzerCreate a call analyzerInfrastructure
update_analyzerUpdate analyzer settingsInfrastructure
create_text_agentCreate a text agentInfrastructure
update_text_agentUpdate text agent settingsInfrastructure
buy_phone_numberPurchase a phone numberInfrastructure
assign_phone_agentAssign a voice agent to a phoneInfrastructure
assign_phone_analyzersAssign analyzers to a phoneInfrastructure
assign_phone_text_agentAssign a text agent to a phoneInfrastructure
{
"action": "create_contact",
"status": "preview",
"preview": {
"first_name": "Jane",
"last_name": "Doe",
"phone_number": "+15551234567",
"tags": ["VIP"]
},
"message": "Ready to create contact Jane Doe. Call again with confirmed=true to proceed."
}
{
"action": "create_contact",
"status": "created",
"contact_id": "a1b2c3d4-...",
"deep_link": "/contacts/a1b2c3d4-...",
"message": "Contact Jane Doe created successfully."
}

The deep_link is generated by entity_deep_link() in page_context_mapper.rs, which maps entity types to their route prefixes.

The ChatMessage component tracks an ActionState for each message that contains a preview:

enum ActionState {
Pending, // Buttons visible — waiting for user decision
Confirming, // User clicked Confirm — "Confirmed" badge shown
Cancelled, // User clicked Cancel — "Cancelled" badge shown
}

Three buttons appear below messages with a preview tool result:

  • Confirm — sends "Yes, go ahead" as a user message, which triggers the AI to re-call the tool with confirmed: true
  • Edit — pre-fills the input with "Yes, but " so the user can add custom instructions
  • Cancel — sends "No, cancel that" as a user message

Buttons only appear when:

  • The tool result has "status": "preview" (detected by ToolResultMeta::parse())
  • The message is not from history (from_history: false)
  • The assistant has finished streaming (is_loading: false)

Historical previews that were never confirmed show an “Expired” badge instead of action buttons.

The ToolCallCard component renders a compact card with a color-coded left accent bar:

StateAccent ColorIndicator
LoadingPrimary (blue)Spinning border
PreviewPending (yellow)Pulsing dot
SuccessSuccess (green)Solid dot
ErrorDestructive (red)Solid dot
OtherMutedSolid dot

Successful actions with a deep_link show an Open button that navigates to the created/modified entity.

Use the make_tool_execute! macro to reduce boilerplate:

pub fn build_create_thing_tool(session: Session) -> Tool {
Tool {
name: AssistantToolName::CreateThing.api_name().to_string(),
description: "Create a thing. Call with confirmed=false to preview, \
then confirmed=true to execute.".to_string(),
input_schema: schemars::schema_for!(CreateThingInput),
execute: make_tool_execute!("create_thing", CreateThingInput, handler => session),
}
}
async fn handler(session: Session, input: CreateThingInput) -> Result<String, AppError> {
// 1. Permission check
// 2. Validate inputs
// 3. If !input.confirmed → return preview JSON
// 4. If confirmed → execute mutation, return success JSON with deep_link
}

The macro handles JSON deserialization, session cloning, and async dispatch through the dedicated TOOL_RUNTIME (a separate multi-threaded Tokio runtime that avoids blocking the single-threaded Dioxus runtime).

FilePurpose
tools/contacts/ai_create_contact_tool.rsReference implementation of confirmation pattern
tools/messages/ai_send_sms_tool.rsConfirmation with conversation context in preview
tools/mod.rsmake_tool_execute! macro definition
tools/runtime.rsTOOL_RUNTIME and run_tool_async()
components/chat_message_component.rsActionState and confirm/edit/cancel buttons
components/tool_call_card_component.rsToolResultMeta parsing and status-colored cards
services/tool_registry_service.rsPermission-gated tool collection
page_context_mapper.rsentity_deep_link() for generating navigation links