Mobile SDK
Orch8 compiles to native iOS and Android libraries via UniFFI. The same Rust engine that runs on servers executes workflows locally on mobile devices — offline-first, battery-aware, with server-side visibility and control.
The mobile runtime is an Orch8-specific execution target. It supports a defined subset of sequence behavior on iOS and Android; use the compatibility matrix below when deciding what can run locally.
Runnable starting point
Clone the maintained example apps, open the iOS project in Xcode or the Android project in Android Studio, configure the sample server URL, and run on a simulator or device.
git clone https://github.com/orch8-io/engine.git
cd engine/mobile-examples
open ios/Orch8Example/Orch8Example.xcodeproj
# Android: open mobile-examples/android in Android StudioWhy mobile workflows?
- ✓Field operations — Technicians following multi-step diagnostic sequences on a tablet — even without connectivity
- ✓Healthcare compliance — Patient intake workflows that must complete reliably and surface approvals to administrators
- ✓Logistics — Delivery drivers running checklists that sync status back to the dispatch dashboard
- ✓Mobile onboarding — Server-configurable user journeys that adapt in real time without app store deployments
Architecture
Mobile workflows follow a simple principle: the device is autonomous, the server is the mailbox.
Mobile Device Server Dashboard
---------------------------------------------------------------------
[run sequences locally]
[accumulate status changes]
[hit wait_for_input]
--POST /mobile/sync----------> [store status updates]
{ status_updates, [store approval requests]
approval_requests, [return pending commands]
command_acks } [return sync_interval_hint]
<----response-----------------
{ commands, sync_interval }
... mobile sleeps ...
[admin sees workflows]
[admin approves/cancels]
[queue command]
[send silent push]
<--silent push notification---
[wake up]
--POST /mobile/sync----------> [drain commands]
<----response-----------------
{ commands: [complete_step] }
[execute command locally]
[complete_step / cancel / etc]Design principles
- —Offline-capable execution — Supported local steps can advance without a connection; sync, remote commands, and server-backed operations wait for connectivity.
- —Server is the mailbox — Stores status, queues commands, dispatches push notifications. Never executes mobile workflows.
- —Batched sync — A sync request can carry status updates, approval requests, command acknowledgements, and pending server commands.
- —Push-driven wake — Mobile sleeps when idle, wakes on silent push notification. No fixed-interval polling.
- —Battery-first — Adaptive sync intervals, state coalescing, power-state multipliers. No unnecessary radio wake-ups.
Sync Protocol
All mobile-server communication flows through a single bidirectional endpoint. The device sends status updates and approval requests; the server returns pending commands and a sync interval hint.
Request: POST /mobile/sync
{
"device_id": "device-abc-123",
"status_updates": [
{
"instance_id": "uuid",
"sequence_name": "onboarding-flow",
"state": "Waiting",
"current_step": "request_approval",
"handler": "request_approval",
"timestamp": "2026-05-19T10:30:00Z",
"context_summary": {}
}
],
"approval_requests": [
{
"instance_id": "uuid",
"block_id": "step-3",
"prompt": "Action requires approval. Please review and decide.",
"choices": [
{ "label": "Approve", "value": "approved" },
{ "label": "Reject", "value": "rejected" }
],
"store_as": "approval_result",
"timeout_seconds": 86400,
"metadata": {}
}
],
"command_acks": ["cmd-001", "cmd-002"]
}Response
{
"commands": [
{
"id": "cmd-003",
"type": "complete_step",
"instance_id": "uuid",
"step_name": "request_approval",
"output": "{\"value\":\"approved\"}"
},
{
"id": "cmd-004",
"type": "cancel_instance",
"instance_id": "uuid-2"
}
],
"sync_interval_secs": 30
}Command types
| Type | Fields | Mobile action |
|---|---|---|
complete_step | instance_id, step_name, output | engine.complete_step(...) |
cancel_instance | instance_id | engine.cancel_instance(...) |
send_signal | instance_id, signal_type, payload | engine.send_signal(...) |
Idempotency
- —Commands are keyed by
id. Mobile ACKs after execution. Server re-delivers unACKed commands on next sync. - —Approval requests are keyed by
(device_id, instance_id, block_id). Duplicate POSTs are ignored. - —Status updates are last-write-wins per
(device_id, instance_id). Latest state overwrites.
Adaptive Sync Intervals
Mobile does not poll on a fixed timer. The sync cycle piggybacks on the engine tick loop via a tick counter. The server dynamically adjusts the interval based on pending work.
| Condition | Interval | Rationale |
|---|---|---|
| Workflows actively advancing | 15s | Keep dashboard current |
| All workflows waiting on approvals | Stop syncing | Wait for push |
| No active workflows | Stop syncing | Nothing happening |
| Push notification received | Immediate sync | Admin acted — pick up commands now |
| Heartbeat fallback | 10-15 min | Catches missed pushes |
| Low battery (PowerState) | 2x interval | Conserve battery |
| App backgrounded | 2x interval, min 60s | OS throttles network anyway |
Push Notifications
Push notifications are a content-less "sync now" signal. They carry no commands or data. The sync endpoint is the single source of truth.
Why push as signal only
- —Push delivery is unreliable (can be dropped, delayed, reordered)
- —Push payload size is limited (4KB APNs, varies FCM)
- —Commands need ACK/retry semantics that push doesn't provide
- —Single source of truth prevents split-brain between push payload and server state
Platform specifics
| Platform | Mechanism | Key detail |
|---|---|---|
| iOS | APNs silent push | content-available: 1, no alert/badge/sound |
| Android | FCM data message | data only, no notification block |
The operating system decides whether and for how long background work runs; silent push is a wake-up hint, not a delivery or runtime guarantee.
Host app responsibility
The engine does NOT handle push registration or receiving. The host app (Swift/Kotlin) must:
- Register for remote notifications with the OS
- Pass the push token to the engine via
register_push_token(token) - On receiving a silent push, call
engine.on_push_received() - The engine wakes its scheduler and starts a sync cycle; actual timing depends on OS scheduling, connectivity, and configured intervals
Approval Flow
When a mobile workflow hits a wait_for_input step, it sends an approval request to the server. An admin resolves it from the dashboard, and the resolution flows back to the device via push + sync.
1. Mobile workflow reaches wait_for_input step
2. Mobile sends approval_request in next sync
3. Server stores approval (state: pending)
4. Dashboard shows approval to admin
5. Admin clicks "Approve"
6. Server resolves approval, creates complete_step command
7. Server sends silent push to device
8. Device wakes, syncs, receives command
9. Device calls engine.complete_step()
10. Workflow continues from where it left offOffline Behavior
When the device is offline, workflows continue executing locally. Status changes and approval requests accumulate in a local SQLite outbox. When connectivity returns, the next sync drains the full outbox in a single request.
- —Workflows advance independently of server availability
- —State changes coalesce — multiple transitions for the same instance collapse to the latest state
- —Sync attempts fail silently (logged, counter reset to retry later)
- —Commands queued on the server are delivered on the first successful sync
Server API Endpoints
| Method | Path | Purpose |
|---|---|---|
| POST | /mobile/sync | Bidirectional sync (status + approvals + commands) |
| POST | /mobile/devices/register | Register push token for a device |
| GET | /mobile/approvals | List pending approvals from all devices |
| POST | /mobile/approvals/{id}/resolve | Admin resolves an approval |
| GET | /mobile/status | List all mobile instance statuses |
| POST | /mobile/commands | Send a command to a specific device |
All endpoints require x-api-key (tenant API key) and x-device-id headers. All queries are scoped by tenant_id for full tenant isolation.
0.7 Device Capability Mesh
A registered device can advertise a short-lived mobile runtime throughPOST /mobile/devices/{device_id}/runtime. The server adds the hardware capabilitydevice:{id} and makes live handler, hardware, region, trust, connectivity, battery, cost, latency, offline, and drain facts available to placement policy.
{
"runtime_kind": "mobile",
"handlers": ["camera.capture", "model.local_inference"],
"hardware": ["camera", "neural_engine"],
"regions": ["br-south"],
"trust": "registered",
"connectivity": "wifi",
"battery_percent": 78,
"estimated_cost_microunits": 0,
"estimated_latency_ms": 40,
"offline": true,
"draining": false
}Placement preview returns the selected runtime, rejected candidates, aplacement_decision_id, andpreview_sha256. Handoff creation must echo both; the engine rechecks live facts at creation and export.
Outbound federation usesPOST /continuity/federation/sign to create a destination-bound envelope with a lifetime of at most five minutes. The caller transports the envelope and unchanged payload to the peer's/continuity/federation/verify endpoint; signing does not perform an implicit network request.
Configuration
Mobile sync is disabled by default. Enable it and configure push notifications via environment variables or TOML config.
# Enable mobile sync
ORCH8_MOBILE_SYNC_ENABLED=true
# Push provider: "apns", "fcm", or "" (disabled)
ORCH8_PUSH_PROVIDER=apns
# APNs configuration
ORCH8_APNS_KEY_PATH=/secrets/apns-auth-key.p8
ORCH8_APNS_KEY_ID=ABC123DEFG
ORCH8_APNS_TEAM_ID=TEAM123456
ORCH8_APNS_TOPIC=io.orch8.example
# Or FCM configuration
# ORCH8_PUSH_PROVIDER=fcm
# ORCH8_FCM_CREDENTIALS_PATH=/secrets/firebase-sa.json
# ORCH8_FCM_PROJECT_ID=my-project-id
# Limits
ORCH8_MOBILE_SYNC_MAX_DEVICES=1000
ORCH8_MOBILE_SYNC_COMMAND_TTL_SECS=604800
ORCH8_MOBILE_SYNC_APPROVAL_TTL_SECS=86400TOML config
[mobile_sync]
enabled = true
max_devices = 1000
command_ttl_secs = 604800
approval_ttl_secs = 86400
[mobile_sync.push]
provider = "apns"
[mobile_sync.push.apns]
key_path = "/secrets/apns-auth-key.p8"
key_id = "ABC123DEFG"
team_id = "TEAM123456"
topic = "io.orch8.example"
sandbox = falseDeployment
Docker
docker run --rm -p 8080:8080 \
-e ORCH8_MOBILE_SYNC_ENABLED=true \
-e ORCH8_PUSH_PROVIDER=apns \
-e ORCH8_APNS_KEY_ID=ABC123DEFG \
-e ORCH8_APNS_TEAM_ID=TEAM123456 \
-e ORCH8_APNS_TOPIC=io.orch8.example \
-v /path/to/apns-key.p8:/secrets/apns-auth-key.p8:ro \
-e ORCH8_APNS_KEY_PATH=/secrets/apns-auth-key.p8 \
ghcr.io/orch8-io/engine:0.7.1No push provider
When ORCH8_PUSH_PROVIDER is empty, the engine uses a no-op provider. Push calls are silently ignored. Mobile relies entirely on heartbeat polling (10-15 min). This is valid for development, staging, or when push infrastructure is unavailable.
Multi-replica
Mobile sync is stateless — any engine replica can serve any device. The database is the coordination point. No sticky sessions needed. Push dispatch is fire-and-forget with heartbeat fallback.
Security
- —Authentication — Same tenant API key as other endpoints. Device identity via x-device-id header (UUID generated on first launch).
- —Tenant isolation — All queries scoped by tenant_id. A device can only see/affect instances belonging to its tenant.
- —Command validation — Server validates commands before queuing — verifies approval exists and is pending, verifies instance belongs to device.
- —Push token security — Push tokens stored server-side only, never exposed via API. Silent push contains no sensitive data.