nautechsystems/nautilus_trader · error · anyhow::Error
Invalid execution transition for intent {intent_id}: {curren
Error message
Invalid execution transition for intent {intent_id}: {current_status} -> {} What it means
Thrown when execution_transition_allowed rejects the requested transition in record_execution_status. The state machine permits prepared to signed/recoverable; signed or broadcast to broadcast/included/replaced/dropped/reorged; included to finalized/reverted/reorged/replaced/dropped; replaced/dropped/reorged to included/finalized/reverted/replaced/dropped/reorged; and same-status repeats (idempotent). finalized, reverted, and recoverable are terminal, so this fires only for genuinely out-of-order or post-terminal observations.
Source
Thrown at crates/adapters/blockchain/src/cache/database.rs:3625
"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 2114cf6f76)
Solutions
- Check whether the observation is stale: same-status repeats are already tolerated, so this error means the event is out of order or the intent is terminal
- Drop delayed observations whose target is no longer reachable from the current status
- Order observations by block_number before recording them
- If a real reorg invalidated a finalized execution, escalate and handle it as a new intent rather than forcing the transition
Example fix
// before: recording whatever arrives first
db.record_execution_status(intent_id, &tx_hash, status, ...).await?;
// after: skip observations the state machine cannot accept
let current = load_intent_status(&pool, intent_id).await?;
if current == status.as_str() {
return Ok(()); // idempotent repeat
}
if !transition_allowed(¤t, status) {
tracing::warn!(%intent_id, %current, ?status, "dropping out-of-order observation");
return Ok(());
}
db.record_execution_status(intent_id, &tx_hash, status, ...).await?; Defensive patterns
Strategy: validation
Validate before calling
// Mirror the persisted state machine before recording
let current: String = sqlx::query_scalar("SELECT status FROM execution_intent WHERE id = $1")
.bind(intent_id)
.fetch_one(&pool)
.await?;
if current != status.as_str() && !transition_allowed(¤t, status) {
// stale or out-of-order observation: drop it
} Type guard
fn transition_allowed(current: &str, next: TransactionStatus) -> bool {
if current == next.as_str() {
return true;
}
match current {
"prepared" => matches!(next, TransactionStatus::Signed | TransactionStatus::Recoverable),
"signed" | "broadcast" => matches!(
next,
TransactionStatus::Broadcast
| TransactionStatus::Included
| TransactionStatus::Replaced
| TransactionStatus::Dropped
| TransactionStatus::Reorged
),
"included" => matches!(
next,
TransactionStatus::Finalized
| TransactionStatus::Reverted
| TransactionStatus::Reorged
| TransactionStatus::Replaced
| TransactionStatus::Dropped
),
"replaced" | "dropped" | "reorged" => !matches!(next, TransactionStatus::Prepared | TransactionStatus::Signed),
_ => false,
}
} Try / catch
match db.record_execution_status(intent_id, &tx_hash, status, block, hash, success, gas, price).await {
Err(e) if e.to_string().contains("Invalid execution transition") => {
// log current->target, classify as stale/terminal, and skip or escalate - never force
}
other => other?,
} Prevention
- Order receipt observations by block_number before recording them
- Treat 'Invalid execution transition' on terminal intents as a signal to escalate (e.g. deep reorg), not a retryable error
- Exploit same-status idempotency: re-recording the current status never throws this error
- Keep a local mirror of the transition table in tests to assert observation orderings
When it happens
Trigger: A late 'included' receipt arriving after the intent already finalized; attempting to move a finalized intent during a reorg; recording broadcast on an intent still 'prepared' (it was never signed); replaying stale observations after crash recovery.
Common situations: RPC or websocket events delivered out of order; restarts replaying old observations; chain reorganizations crossing the finalize boundary; duplicate events with differing block data.
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
- Failed to reserve execution intent for signer {} on chain {}
- Execution intent {intent_id} is not prepared for nonce {nonc
- Failed to start execution schema migration: {e}
- Failed to lock legacy execution transactions: {e}
- Failed to migrate execution_transaction table: {e}
AI-assisted analysis of nautechsystems/nautilus_trader@2114cf6f76 (2026-08-21).
Data as JSON: /api/errors/7f1279940c245a30.
Report an issue: GitHub.