---
title: 'Troubleshooting: HITL or checkpoint stuck'
path: troubleshooting/hitl-stuck
status: published
---

# Troubleshooting: HITL or checkpoint stuck

## Invocation pauses at HITL Review but no review appears

The flow's HITL Review compiled to a `@checkpoint` with `hitl_target: {scope, queue, hitl_spec}`. ScaiCore's runtime should detect the field, auto-publish to ScaiQueue, and the message should appear in the review UI.

Check, in this order:

### 1. Is the checkpoint actually in `pending` state?

```bash
curl -s "https://scaiflow.example/api/v1/scaicore/checkpoints?core_id=$CORE_ID&status=pending" \
  -H "Authorization: Bearer $TOKEN"
```

If a row exists with `status: "pending"` and `checkpoint_type: "hitl_review"`, the runtime hit the checkpoint correctly. The publish step is next.

### 2. Did the runtime auto-publish to ScaiQueue?

List recent messages in the configured scope:

```bash
curl -s "https://scaigrid.scailabs.ai/v1/modules/scaiqueue/scopes/$SCOPE_ID/queues/$QUEUE_ID/messages?limit=10" \
  -H "Authorization: Bearer $SCAIGRID_TOKEN"
```

If a `hitl_request` message appeared with `correlation_id` matching the checkpoint id, the publish worked. The reviewer just hasn't seen it yet — direct them to the ScaiQueue review UI.

If no message: ScaiCore's auto-publish didn't fire. Check that the deployed Core's runtime version supports `hitl_target` (≥ v1.2.0). Re-deploy with a fresh wheel install of the compiler+runtime.

### 3. `hitl_target.scope` is null

If the IR has `hitl_target.scope = null`, the runtime can't route the publish. Caused by an HITL Review node with no `scope` config AND no flow-level `flow.config.scaiqueue.scope` to inherit from.

**Fix:** Either set `scope` directly on the HITL Review node, OR declare a flow-level ScaiQueue topology in Flow → ScaiQueue and let the node inherit.

## Reviewer clicked "approve" but the invocation is still paused

Two possible causes:

### Decision didn't reach ScaiCore

The reviewer's response is published as a `scaiqueue.message.completed` event. ScaiCore subscribes and resolves the checkpoint when it arrives.

Check `GET /v1/scaicore/events?core_id=$CORE_ID` for a `scaiqueue.message.completed` event. If absent, the ScaiQueue completion didn't propagate — check ScaiQueue + ScaiCore connectivity.

### Resolution string doesn't match what the flow expects

The decision section's option `id`s (e.g. `approve`, `reject`) are what the runtime injects as the checkpoint's resolution. If the flow's downstream branch checks `decision == "Approve"` (capitalized), it won't match.

**Fix:** Ensure decision option ids match the conditions that consume them. Conventionally lowercase, no spaces.

## Manual resolve via API

If a checkpoint is stuck and you want to force it:

```bash
curl -X POST "https://scaiflow.example/api/v1/scaicore/checkpoints/$CHECKPOINT_ID/resolve" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"resolution": "approve", "comment": "Manual resolve — bypassed review UI"}'
```

Requires `scaicore:manage`. Useful for unblocking stuck invocations during ops triage.

## Debug breakpoint won't resume

Pending breakpoints bar in the canvas's Live Runs panel shows the paused invocation. Click "Continue" → calls `POST /v1/scaicore/checkpoints/{id}/resolve` with `{"resolution": "continue"}`.

If the click does nothing:

- Check console for a 401/403 — your token might have expired (refresh).
- Check the checkpoint actually has `checkpoint_type: "debug_breakpoint"` (HITL checkpoints need a full decision body, not just "continue").
- The breakpoint flag on the node config should be unset for the next invocation, otherwise the flow pauses again. Toggle the node's breakpoint off + redeploy.

## My HITL form fields aren't being filled with my flow variables

Template expressions in section fields use `{{variable}}` syntax. For example:

```jsonc
{ "type": "input", "field": "amount", "kind": "currency", "default": "{{amount}}" }
```

The runtime resolves `{{amount}}` against the flow's current scope at the moment the checkpoint is hit. If the variable isn't bound yet (e.g. nothing upstream produced an `amount`), the default is empty.

**Fix:** Make sure an upstream block bound `amount` to its result (e.g. an `llm_flexible` node with `result_binding: "amount"`, or a `data_variable_set` node). The Live Runs panel shows bound values on each block's exited event.
