Skip to content

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.

No other workflow engine runs on mobile. Temporal is JVM-based. Inngest is cloud-only. Orch8 is the only orchestration engine that works on phones.

Why mobile workflows?

  • Field operationsTechnicians following multi-step diagnostic sequences on a tablet — even without connectivity
  • Healthcare compliancePatient intake workflows that must complete reliably and surface approvals to administrators
  • LogisticsDelivery drivers running checklists that sync status back to the dispatch dashboard
  • Mobile onboardingServer-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

  • Mobile stays autonomousSequences execute locally. Mobile never depends on server availability to advance a workflow.
  • Server is the mailboxStores status, queues commands, dispatches push notifications. Never executes mobile workflows.
  • Single round-trip per syncOne HTTP request carries everything in both directions — status updates, approval requests, and command acknowledgements.
  • Push-driven wakeMobile sleeps when idle, wakes on silent push notification. No fixed-interval polling.
  • Battery-firstAdaptive 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

JSON
{
  "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

JSON
{
  "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

TypeFieldsMobile action
complete_stepinstance_id, step_name, outputengine.complete_step(...)
cancel_instanceinstance_idengine.cancel_instance(...)
send_signalinstance_id, signal_type, payloadengine.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.

ConditionIntervalRationale
Workflows actively advancing15sKeep dashboard current
All workflows waiting on approvalsStop syncingWait for push
No active workflowsStop syncingNothing happening
Push notification receivedImmediate syncAdmin acted — pick up commands now
Heartbeat fallback10-15 minCatches missed pushes
Low battery (PowerState)2x intervalConserve battery
App backgrounded2x interval, min 60sOS 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

PlatformMechanismKey detail
iOSAPNs silent pushcontent-available: 1, no alert/badge/sound
AndroidFCM data messagedata only, no notification block

Both wake the app in background for ~30 seconds — enough for one sync round-trip.

Host app responsibility

The engine does NOT handle push registration or receiving. The host app (Swift/Kotlin) must:

  1. Register for remote notifications with the OS
  2. Pass the push token to the engine via register_push_token(token)
  3. On receiving a silent push, call engine.on_push_received()
  4. The engine triggers an immediate sync cycle on the next tick (~200ms)

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 off

Offline 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#

MethodPathPurpose
POST/mobile/syncBidirectional sync (status + approvals + commands)
POST/mobile/devices/registerRegister push token for a device
GET/mobile/approvalsList pending approvals from all devices
POST/mobile/approvals/{id}/resolveAdmin resolves an approval
GET/mobile/statusList all mobile instance statuses
POST/mobile/commandsSend 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.

Configuration#

Mobile sync is disabled by default. Enable it and configure push notifications via environment variables or TOML config.

Bash
# 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=86400

TOML 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 = false

Deployment#

Docker

Bash
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:latest

No 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#

  • AuthenticationSame tenant API key as other endpoints. Device identity via x-device-id header (UUID generated on first launch).
  • Tenant isolationAll queries scoped by tenant_id. A device can only see/affect instances belonging to its tenant.
  • Command validationServer validates commands before queuing — verifies approval exists and is pending, verifies instance belongs to device.
  • Push token securityPush tokens stored server-side only, never exposed via API. Silent push contains no sensitive data.