nautechsystems/nautilus_trader · error · anyhow::Error

Invalid execution transition for intent

Error message

Invalid execution transition for intent {intent_id}: {current_status} -> {}

What it means

This error comes from anyhow::ensure! after the state machine check `execution_transition_allowed(&current_status, status)` fails. It means the requested status transition is not part of the allowed execution-intent lifecycle (e.g. Pending -> Finalized skipped intermediate states, or moving a terminal status). The transaction is aborted, leaving the stored status unchanged.

Solutions

  1. Log/inspect current_status and the requested status; confirm the event matches the intent's actual lifecycle stage.
  2. Make processing idempotent: skip transitions already applied (check fill_emitted/terminal_emitted flags before calling).
  3. Deduplicate events upstream (track processed tx hashes/block numbers) to avoid double-delivery.
  4. If this fires on replay after a version upgrade, review execution_transition_allowed for newly added/renamed statuses and migrate stale rows.

Example fix

// before
anyhow::ensure!(
    execution_transition_allowed(&current_status, status),
    "Invalid execution transition for intent {intent_id}: {current_status} -> {}",
    status.as_str()
);
// after
if current_status == status.as_str() || terminal_emitted {
    tracing::debug!(%intent_id, %current_status, "transition already applied; treating as idempotent no-op");
    transaction.commit().await?;
    return Ok(());
}
anyhow::ensure!(
    execution_transition_allowed(&current_status, status),
    "Invalid execution transition for intent {intent_id}: {current_status} -> {}",
    status.as_str()
);
Defensive patterns

Strategy: validation

Validate before calling

// Idempotency check before requesting a transition
let (current, fill_emitted, terminal_emitted) = fetch_intent_state(&pool, &intent_id).await?;
if terminal_emitted || current == status.as_str() {
    return Ok(()); // already applied; no-op
}

Try / catch

if let Err(e) = db.record_execution_status(&intent_id, status, ...).await {
    if e.to_string().contains("Invalid execution transition") {
        tracing::debug!(%intent_id, "stale/duplicate transition; treating as no-op");
        return Ok(());
    }
    return Err(e);
}

Prevention

When it happens

Trigger: Calling the transition method with a target status that is illegal from the row's current status — e.g. delivering a receipt/finality event twice, events arriving out of order, or the row already terminal (Filled/Reverted/Finalized) and another update arriving.

Common situations: Duplicate block/receipt processing after a node reconnect or replay; two workers racing on the same intent (one wins, the other's stale status check fails); a status enum/version change making old persisted statuses invalid for new transition rules; feeding reverted receipts into an intent expected to fill.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of nautechsystems/nautilus_trader@18893faf8b (2026-09-08). Data as JSON: /api/errors/7f1279940c245a30. Report an issue: GitHub.

Appendix: source

Thrown at crates/adapters/blockchain/src/cache/database.rs:6793

                "Execution gas used {} exceeds PostgreSQL BIGINT",
                gas_used.unwrap_or_default()
            )
        })?;
        let mut transaction = self
            .pool
            .begin()
            .await
            .map_err(|e| anyhow::anyhow!("Failed to start execution status transition: {e}"))?;
        let (current_status, fill_emitted, terminal_emitted) =
            sqlx::query_as::<_, (String, bool, bool)>(
            "SELECT status, fill_emitted, terminal_emitted FROM execution_intent WHERE id = $1 FOR UPDATE",
        )
        .bind(intent_id)
        .fetch_optional(&mut *transaction)
        .await
        .map_err(|e| anyhow::anyhow!("Failed to lock execution intent {intent_id}: {e}"))?
        .ok_or_else(|| anyhow::anyhow!("Execution intent {intent_id} was not found"))?;
        anyhow::ensure!(
            execution_transition_allowed(&current_status, status),
            "Invalid execution transition for intent {intent_id}: {current_status} -> {}",
            status.as_str()
        );

        let active = match status {
            TransactionStatus::Finalized | TransactionStatus::Reverted => {
                !fill_emitted && !terminal_emitted
            }
            TransactionStatus::Recoverable => false,
            _ => true,
        };
        let hash_result = sqlx::query(
            "
            UPDATE execution_transaction_hash
            SET status = $3,
                block_number = COALESCE($4, block_number),
                block_hash = COALESCE($5, block_hash),

View on GitHub (pinned to 18893faf8b)