Phone Approvals For An AI Agent: How `mobile.ask` Works
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
- Agent calls
mobile.ask(...)— writesowners/{uid}/approvals/{id}withstatus: pending. - Cloud Function detects the new doc; queries
owners/{uid}/channels/*; sends FCM push for each. - Phone receives push. Notification: prompt + detail (truncated). Long-press → quick action: Approve / Deny.
- User taps Approve. App calls
decide(id, 'approved'). Cloud Function writesstatus: approved. - Agent's listener (Firestore snapshot on the doc) fires. Agent resumes.
Latency
| Step | Typical |
|---|---|
| Write doc → CF trigger | ~50ms |
| CF → FCM | ~100ms |
| FCM → phone | 200–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).