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(¤t_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
- Log/inspect current_status and the requested status; confirm the event matches the intent's actual lifecycle stage.
- Make processing idempotent: skip transitions already applied (check fill_emitted/terminal_emitted flags before calling).
- Deduplicate events upstream (track processed tx hashes/block numbers) to avoid double-delivery.
- 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(¤t_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(¤t_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
- Track processed tx hashes/block numbers to deduplicate receipt events.
- Single-writer per intent or rely on FOR UPDATE serialization.
- Check fill_emitted/terminal_emitted flags before emitting duplicate transitions.
- Review transition rules when upgrading status enums and migrate stale rows.
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
- Execution intent is , not prepared for signing
- Execution intent is not prepared for nonce
- Active replacement scan intent was not found
- Execution payload storage is not ready for protected…
- Execution payload storage is not rolling back
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(¤t_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)