Blog

Phone Approvals For An AI Agent: How `mobile.ask` Works

Akshay Sarode
Direct answer

Agent calls mobile.ask(prompt, detail, ttlSec). Approval lands in owners/{uid}/approvals/{id}. FCM push fires to registered channel. Human approves on phone (long-press notification → Face ID). Agent's snapshot listener fires; agent resumes. Total latency: 0.5–2s.

Most agent loops don't have a "wait for human" primitive. They tool-call straight through. The cost is at the edges — the moment the agent should ask "is this OK?" it just doesn't, and either you write a custom approval queue or you accept that the agent makes some bad calls.

Ujex's mobile.ask is the primitive. ~30 lines of agent code; ~50 lines of human config; everything else is the platform.

Agent side

from ujex_mobile import Mobile
m = Mobile(api_key=os.environ['UJEX_API_KEY'], agent_id='abc')

approval = m.ask(
    prompt='Push to main?',
    detail='Will deploy commit 4f3a91 to production',
    ttl_sec=300,
)

# Blocks (or polls; you choose)
result = approval.wait()  # returns {'status': 'approved'|'denied'|'timeout'}

if result['status'] == 'approved':
    git_push()
else:
    log.warn('approval denied, holding')

Human side: register a channel once

# From phone (one-time setup)
channels.register(uid='your-uid', kind='fcm', token=fcm_device_token)

The full flow

  1. Agent calls mobile.ask(...) — writes owners/{uid}/approvals/{id} with status: pending.
  2. Cloud Function detects the new doc; queries owners/{uid}/channels/*; sends FCM push for each.
  3. Phone receives push. Notification: prompt + detail (truncated). Long-press → quick action: Approve / Deny.
  4. User taps Approve. App calls decide(id, 'approved'). Cloud Function writes status: approved.
  5. Agent's listener (Firestore snapshot on the doc) fires. Agent resumes.

Latency

StepTypical
Write doc → CF trigger~50ms
CF → FCM~100ms
FCM → phone200–800ms
Human reaction (phone in hand)1–5s
Approve write → agent listener~50ms
Total (phone in hand)1.5–6s

Auth on approve

The phone app uses Face ID / Touch ID / device passcode for the actual approve gesture. The push can be tapped without auth (just shows the prompt); the approve action requires the biometric.

TTL

If no decision within ttlSec, the doc transitions to status: timeout. Agent's listener fires; the agent gets a denial-equivalent. This is a feature — agents shouldn't wait forever on a human who isn't around.

Multiple approvers

Push fans out to all registered channels for the owner. First-responder wins. Once decided, subsequent taps see "already decided." Useful for teams with rotating on-call.

Audit

Every step writes an audit row: ask, push, decide, timeout. The hash-chained log lets you reconstruct "who approved what when" for any agent action.

SMS fallback

For users who don't want the app: register an SMS channel. The push gets sent as a Twilio SMS with a one-time approval URL (single-use, TTL'd). Same security; slower.

FAQ

Can I approve from my watch?

Yes — the FCM notification mirrors to watchOS. Long-press to approve. We don't ship a separate watch app.

What if no one is registered?

mobile.ask returns status: no_channel immediately. Agent should treat this as denial.

Can the agent ask the same question twice?

Each ask creates a new approval doc. Idempotency is the agent's responsibility (use a deterministic ID if you want dedup).