> ## Documentation Index
> Fetch the complete documentation index at: https://jephalabs.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# First Movement

> Create and execute your first dry-run movement in QTG

# 02 - First Movement (Dry-Run)

> Experience the full flow in dry-run mode: register a template -> create a movement -> approve it -> check status.
> Confirm behavior through the API layer only, without live exchange integration.

**Prerequisite:** In [01-local-setup.md](/quickstart/01-local-setup), the server is running on `localhost:8100` with `MG_AUTH_ENABLED=false` and `MG_WORKERS_ENABLED=false`.

***

## Full Flow

```
1. Register template    POST /v3/plan-templates
2. Create movement      POST /v3/movements
3. Approve movement     POST /v3/movements/{id}/approve
4. Check status         GET  /v3/movements/{id}
```

***

## Step 1: Register a Template

Register a CEX 3-node template (Upbit -> Bybit XRP):

```bash theme={null}
curl -s -X POST http://localhost:8100/v3/plan-templates \
  -H "Content-Type: application/json" \
  -d '{
    "template_key": "cex.upbit_to_bybit.xrp",
    "name": "Upbit to Bybit XRP",
    "version": 1,
    "nodes": [
      {
        "node_key": "withdraw",
        "node_kind": "action",
        "action_type": "cex_withdrawal",
        "executor_selector": {"action_type": "cex_withdrawal"},
        "config": {"asset": "XRP", "source_exchange": "upbit", "destination_exchange": "bybit", "network": "XRP"},
        "has_side_effect": true,
        "timeout_policy": {"submit_seconds": 120},
        "retry_policy": {},
        "risk_patch": {}
      },
      {
        "node_key": "withdraw_observe",
        "node_kind": "observe",
        "action_type": "cex_withdrawal_status",
        "executor_selector": {"action_type": "cex_withdrawal_status"},
        "config": {"asset": "XRP", "source_exchange": "upbit"},
        "has_side_effect": false,
        "timeout_policy": {"observe_seconds": 1800},
        "retry_policy": {},
        "risk_patch": {}
      },
      {
        "node_key": "deposit_observe",
        "node_kind": "observe",
        "action_type": "cex_deposit_status",
        "executor_selector": {"action_type": "cex_deposit_status"},
        "config": {"asset": "XRP", "destination_exchange": "bybit", "match_mode": "txid"},
        "has_side_effect": false,
        "timeout_policy": {"observe_seconds": 3600},
        "retry_policy": {},
        "risk_patch": {}
      }
    ],
    "edges": [
      {"from": "withdraw", "to": "withdraw_observe", "edge_type": "on_success"},
      {"from": "withdraw_observe", "to": "deposit_observe", "edge_type": "on_success"}
    ],
    "approval_policy": {"mode": "manual_first"},
    "completion_policy": {"completion_assurance": "destination_credited"},
    "risk_controls": {"asset_allowlist": ["XRP"], "max_amount": "1000"}
  }' | python -m json.tool
```

> **`risk_controls` is free-form, and only `max_amount` binds.** `max_amount` is
> enforced fail-closed at dispatch. `asset_allowlist` is descriptive — it is pinned into
> the plan hash, but nothing rejects a movement whose `intent.asset` falls outside it.
> See the [v3 endpoints reference](/reference/api/v3-endpoints) for the full enforced set.

Check these fields in the success response:

* `graph_shape`: `"linear"` - executable in v3.0
* `executable_in_v3_0`: `true`
* `topological_order`: `["withdraw", "withdraw_observe", "deposit_observe"]`

## Step 2: Create a Movement

Create a movement from the registered template:

```bash theme={null}
curl -s -X POST http://localhost:8100/v3/movements \
  -H "Content-Type: application/json" \
  -d '{
    "template_key": "cex.upbit_to_bybit.xrp",
    "template_version": 1,
    "intent": {
      "asset": "XRP",
      "amount": "25",
      "source": "upbit",
      "destination": "bybit"
    },
    "input_params": {
      "asset": "XRP",
      "amount": "25",
      "network": "XRP",
      "destination_address": "rBybitXRP...",
      "destination_tag": "12345"
    },
    "callback": {"url": "http://localhost:9999/callback"}
  }' | python -m json.tool
```

Check these fields in the response:

* `movement_id`: UUID - used in later API calls
* `request_state`: `"PENDING_APPROVAL"` - waiting for approval
* `compiled_plan_hash`: `"sha256:..."` - required for approval

> **You can create a movement without `callback`.** It still works, but no state-change notifications are sent. The callback URL is a nested field — `callback: {"url": "..."}`, not a top-level `callback_url` (a top-level `callback_url` is silently ignored).

## Step 3: Approve the Movement

Send the approval request with `compiled_plan_hash`:

```bash theme={null}
# Copy movement_id and compiled_plan_hash from the response above
MOVEMENT_ID="<movement_id>"
PLAN_HASH="<compiled_plan_hash>"

curl -s -X POST "http://localhost:8100/v3/movements/${MOVEMENT_ID}/approve" \
  -H "Content-Type: application/json" \
  -d "{
    \"approver_id\": \"quickstart-operator\",
    \"compiled_plan_hash\": \"${PLAN_HASH}\"
  }" | python -m json.tool
```

Check these fields in the response:

* `request_state`: `"APPROVED"` -> if workers are enabled, it transitions to `"EXECUTING"`
* `approval_status`: `"approved"`

> **Why `compiled_plan_hash` is required:** it guarantees the plan was not altered between creation and approval.
> Sending the wrong hash returns `409 Conflict`.

## Step 4: Check Status

```bash theme={null}
curl -s "http://localhost:8100/v3/movements/${MOVEMENT_ID}" | python -m json.tool
```

In dry-run mode (`MG_WORKERS_ENABLED=false`):

* `request_state` stays at `"APPROVED"` (because the dispatcher is off)
* Node state stays `"BLOCKED"` (the first frontier node) or remains inactive

If workers are enabled and exchange credentials are configured:

* `request_state`: `"EXECUTING"` -> `"COMPLETED"`
* Node states advance per node kind. The representative path for a **signing on-chain action** node is `BLOCKED` -> `READY` -> `PREPARING` -> `AWAITING_SIGNATURE` -> `SUBMITTING` -> `SUBMITTED` -> `OBSERVING` -> `COMPLETED`. Common variants: a node needing no signature skips `AWAITING_SIGNATURE`; a sync-terminal submit (e.g. `cex_withdrawal`) goes `SUBMITTING` -> `COMPLETED` directly; an observe-only node (e.g. `cex_deposit_status`) goes `READY` -> `OBSERVING` -> `COMPLETED`.

## Step 5: List Movements

```bash theme={null}
# Full movement list
curl -s "http://localhost:8100/v3/movements" | python -m json.tool

# Filter by state
curl -s "http://localhost:8100/v3/movements?state=PENDING_APPROVAL" | python -m json.tool
```

***

## Python Client Example

```python theme={null}
import httpx

BASE = "http://localhost:8100"

# 1. Register template
resp = httpx.post(f"{BASE}/v3/plan-templates", json={
    "template_key": "cex.upbit_to_bybit.xrp",
    "name": "Upbit to Bybit XRP",
    "version": 1,
    "nodes": [...],  # See the JSON above
    "edges": [...],
    "approval_policy": {"mode": "manual_first"},
    "completion_policy": {"completion_assurance": "destination_credited"},
    "risk_controls": {"asset_allowlist": ["XRP"]},
})
print(resp.json())

# 2. Create movement
resp = httpx.post(f"{BASE}/v3/movements", json={
    "template_key": "cex.upbit_to_bybit.xrp",
    "template_version": 1,
    "intent": {"asset": "XRP", "amount": "25"},
    "input_params": {"asset": "XRP", "amount": "25", "network": "XRP"},
    # Optional: "callback": {"url": "http://localhost:9999/callback"}
})
data = resp.json()
movement_id = data["movement_id"]
plan_hash = data["compiled_plan_hash"]

# 3. Approve
resp = httpx.post(f"{BASE}/v3/movements/{movement_id}/approve", json={
    "approver_id": "quickstart",
    "compiled_plan_hash": plan_hash,
})
print(resp.json()["request_state"])  # APPROVED

# 4. Check status
resp = httpx.get(f"{BASE}/v3/movements/{movement_id}")
print(resp.json()["request_state"])
```

***

## Movement State Flow Summary

```
PENDING_APPROVAL -> APPROVED -> EXECUTING -> COMPLETED
                                        -> FAILED
                 -> REJECTED (when rejected)
```

* **PENDING\_APPROVAL**: Waiting for approval. `POST .../approve` or `POST .../reject`
* **APPROVED**: Approved. If workers are enabled, it automatically transitions to EXECUTING
* **EXECUTING**: Nodes are running
* **COMPLETED**: All nodes finished
* **FAILED**: An unrecoverable failure occurred

> For the full state machine, see the [States and Transitions reference](/reference/domain/states-and-transitions).

***

## Troubleshooting

| Symptom                                  | Cause                                     | Fix                                                                                                                               |
| ---------------------------------------- | ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------- |
| `422 completion_assurance is required`   | `completion_policy` missing or misspelled | Valid values: `provider_completed`, `destination_credited`, `destination_observed`, `destination_finalized`, `protocol_finalized` |
| `422 template graph must be a DAG`       | Cycle exists in the edges                 | Check edge direction                                                                                                              |
| `422 edge_type` error                    | Using `"then"`                            | Use `"on_success"`                                                                                                                |
| `409 compiled plan hash mismatch`        | Hash mismatch at approval                 | Pass `compiled_plan_hash` exactly from the movement-create response                                                               |
| `409 template is not executable in v3.0` | Branching/merging graph                   | v3.0 executes only linear graphs                                                                                                  |

***

**Next:** [03-template-cookbook.md](/quickstart/03-template-cookbook) - template examples by lane
