Smart Order Capture
Reference

Workflow DSL

The JSON shape every workflow takes — the single source of truth shared between the web builder, the API validator, and the on-device Kotlin interpreter.

Workflows are stored as JSON validated against a Zod schema. The same schema lives at the API layer (rejecting bad input) and is mirrored as Kotlin data classes via codegen (see scripts/ts-to-kotlin.ts). The current dslVersion is 1.

Workflow shape

{
  "dslVersion": 1,
  "name": "Morning routine",
  "description": "Open Spotify, wait, show a brief.",
  "nodes": [
    {
      "id": "t1",
      "kind": "trigger.time",
      "cron": "0 7 * * 1-5",
      "timezone": "America/New_York"
    },
    { "id": "a1", "kind": "action.openApp", "packageName": "com.spotify.music" },
    { "id": "a2", "kind": "action.wait", "durationMs": 60000 },
    { "id": "a3", "kind": "action.showToast", "message": "Good morning" }
  ],
  "edges": [
    { "id": "e1", "source": "t1", "target": "a1" },
    { "id": "e2", "source": "a1", "target": "a2" },
    { "id": "e3", "source": "a2", "target": "a3" }
  ],
  "enabled": true
}

Top-level fields

  • dslVersion (number, required) — must equal the current DSL version.
  • name (string, 1..120) — human label.
  • description (string?, ≤2000) — optional details.
  • nodes (Node[], 1..200) — at least one trigger required.
  • edges (Edge[], ≤400) — directed connections between node ids.
  • enabled (boolean) — whether the workflow is armed; defaults to true.

Node shape

Every node has id (string), an optional label and position, plus a kind discriminator. The kind determines which additional fields are valid. See triggers and actions for the full per-kind schemas.

Edge shape

{
  "id": "edge_id",
  "source": "<node id>",
  "target": "<node id>",
  "branch": "true" | "false"     // only when source is an action.branch
}

Variables and branches

An action.httpCall with storeResponseAs: "foo" populates a variable named foo in the run scope. Downstream action.branch nodes can read it:

{
  "id": "branch1",
  "kind": "action.branch",
  "condition": {
    "variable": "responseBody",
    "op": "contains",
    "value": "\"ok\":true"
  }
}

Operators: eq, neq, gt, gte, lt, lte, contains, matches (regex). The value is a JSON primitive (string, number, or boolean).

Trace shape

Every run produces a trace — an ordered list of events, one per node executed. Trace events look like this:

{
  "at": 1778953600421,
  "nodeId": "a1",
  "kind": "action.openApp",
  "outcome": "ok",
  "durationMs": 234
}

outcome is one of ok, skipped, or failed. On failure, the event carries a message string.

Note
The trace shape is byte-equal between the TypeScript reference interpreter (used by the builder's live preview) and the Kotlin interpreter (running on your phone). CI golden- tests this against fixtures in packages/workflow-engine/fixtures/.

Validation rules worth knowing

  • Node ids must be unique within a workflow.
  • Every edge must reference existing node ids.
  • At least one trigger node is required.
  • Package-name fields must match Android's package format (com.foo.bar).
  • Workflows targeting denylisted packages are refused — both at workflows.create on the server and pre-dispatch on-device.

Versioning

New node kinds bump DSL_VERSION. The Kotlin interpreter handles unknown kinds gracefully — it logs a warning and refuses to execute the run, prompting the user to update the app. Older workflows continue to validate against newer DSL versions as long as their node kinds are still supported.

JSON schema

Until we publish a formal JSON Schema, the canonical definition lives in packages/shared/src/dsl.ts. Examples for every node kind live in the same directory under __fixtures__/ and are exercised in CI.