ErrLookup › Background articles › "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
"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
Invalid state transition errors ("status must be '%s', actually '%s'", "MCP server is already rejected.", "Cart is already charging!", "Cannot update task with status: completed", "illegal ... job transition from queued to completed") fire when code triggers an operation from a lifecycle state that does not allow it — a second start(), a retried rejection, a step run out of order, or a write to an already-terminal record. This article explains the state-machine mechanism behind the family, the triggers that recur across libraries, and the checks that prevent them.
Distilled from 116 documented records across 31 repositories.
Background
These errors come from explicit state machines embedded inside libraries. The library enumerates an object's statuses and defines which transitions are legal: NautilusTrader's ComponentState::transition accepts only enumerated (state, trigger) pairs, claude-mem ships an ALLOWED_JOB_TRANSITIONS map, NautilusTrader's execution layer backs a transition table (execution_transition_allowed) with database guards, and Phabricator's Drydock hard-codes status preconditions such as STATUS_PENDING before acquireOnResource(). When a call arrives whose (current state, requested operation) pair is not in the table, the guard throws before side effects happen — NautilusTrader rejects a replacement recording atomically before any writes, and Gumroad's rich-content guards fire inside the transaction after a product lock.
The guards exist to protect invariants the library cannot verify any other way: exactly one in-flight charge per Phortune cart, at most one fill/terminal marker per execution intent, no graph mutations after GitNexus has streamed relationship rows to CSV, terminal task records frozen against reassignment in ruflo. Because the check is an invariant rather than a transient condition, Phabricator explicitly documents that retrying the identical call on the same object fails identically — the state must be reconciled, not re-attempted. Vector's fanout shows the same idea at component scale: a Replace control message for a sink id that was never Added (or was already Removed) panics because the control-message sequence itself is invalid.
From the caller's side the messages cluster into recognizable shapes: "status must be X, actually Y" (Drydock), "already X" (Phortune's "Cart is already charging!", LiteLLM's "MCP server is already rejected.", Paperclip's "Plugin ... is already uninstalled"), "cannot X while/with Y" (React DevTools' "Profiling data cannot be updated while profiling is in progress.", OpenProject's import run "cannot be removed while it is running"), and explicit from-to forms ("illegal observation generation job transition from queued to completed", "Invalid state trigger Running -> Start"). They surface as exceptions, HTTP 400 responses, or — for Vector — a panic that indicates an internal bug to report upstream.
Where libraries disagree is idempotency and severity, and that behavior is library-specific: NautilusTrader tolerates same-status repeats as idempotent but throws on genuinely out-of-order transitions; GitNexus's plain KnowledgeGraph.removeRelationship returns false for unknown ids while the streaming sink throws on any id; Paperclip's docs suggest treating a 400 "already uninstalled" as success in automation, while NautilusTrader's marker guard is documented as a signal of dispatcher logic bugs, not a transient fault. Deciding which variant you face — retryable after state advances, idempotent no-op, or logic bug — is the first step of every fix in this family.
Common causes
- Operation issued from a state that does not allow it. The caller never inspected the current state before acting: start() on an already-Running component, acquireOnResource() on a non-PENDING lease, resumeWorkflow() on a running or completed workflow, accept on a call that is still queued. The state machine rejects the (state, trigger) pair because it is simply not in the table.
- Duplicate or retried operation. The first call already moved the state, and the second one arrives late: double-click on checkout ("Cart is already charging!"), a second DELETE without purge on an uninstalled plugin, rejecting an already-rejected MCP server, acquiring a lease twice. A retry whose first response was lost can commit the transition and still throw on replay.
- Concurrent actors racing on one entity. Two workers, tabs, admins, or webhooks act on the same record between check and write: a task completed between status check and assign, two daemons activating the same lease, duplicate Meta webhooks corrupting a call's status, an orchestrator and Vector's fanout disagreeing on which sinks exist during reload.
- Steps executed out of order or skipped. The sequence has mandatory intermediate states: marking a job completed while still queued (skipping processing), activating a lease that was never acquired, calling acceptHandoff before requestHandoff, starting Rocket.Chat's import before the user-selection step, judging a trajectory that was never completed.
- Operating on a terminal record. The entity reached a terminal state and is frozen: assigning or updating a completed/cancelled ruflo task, unplanning a manufacturing order with started work orders, recording a replacement on a finalized or reverted execution intent, resuming a failed workflow. Terminal states accept no outgoing transitions.
- Stale in-memory or cached state. The object in hand predates another process's transition: a lease loaded before another daemon released it, a product whose shared-content flag changed since the last read, an import status that advanced between read and action. The guard re-checks state at the database or under lock, so the cached view loses.
- Event arrives after a one-way boundary. Some gates cannot be reopened: removeRelationship after GitNexus started streaming to CSV, a late 'included' receipt after an intent finalized, PayPal callbacks replayed after the active charge was consumed. The library throws instead of pretending the operation succeeded.
- Ambiguous migration with two sources of truth. Flipping a mode flag is refused when both sides hold data: Gumroad rejects switching to shared rich content when product-level and variant-level content both exist, or when multiple variants hold non-identical content — no single winner exists, so the migration refuses rather than picking silently.
What usually fixes it
- Verify state immediately before the call, on fresh data: use the library's predicates (is_running(), is_stopped(), status === 'paused'), re-fetch the record from the database rather than a cached object, and re-check right before dispatch in racy flows.
- Read the message before deciding the response, and make duplicate transitions idempotent where the goal is the end state: treat 400 "already rejected"/"already uninstalled" as a no-op success in automation, but treat infrastructure-failure messages as errors to fix — opposite responses to similar-looking errors.
- Give each entity's lifecycle a single owner: serialize lifecycle commands through one task, timer, or dispatcher so triggers cannot interleave inside transitional states, and make dispatch deterministic (one dispatcher per intent, one admin per submission).
- Follow the library's canonical sequence and encode it in helpers so steps cannot be skipped: acquire before activate, pause before resume, claim (queued to processing) before completing, request handoff before accepting it, complete trajectories before judging them.
- Do not resurrect terminal entities: create a new task, workflow, lease, or component carrying the old metadata instead of forcing a transition out of a terminal state; where reuse is supported, use the defined reset path (e.g. Stopped to Resetting to Ready) before starting again.
- Never bypass the guard by writing status directly or reordering phases around a one-way gate: the transition table exists to keep invariants and audit trails consistent — if the guard fires on a legitimate flow, fix the ordering or escalate as the library prescribes.
Go deeper
- Authentication and authorization failures — expired tokens, bad credentials, and missing scopes.
Documented occurrences
- Invalid state trigger {self} -> {trigger} (nautechsystems/nautilus_trader)
- Cannot remove relationship "${relationshipId}": it has already been streamed to CSV and cannot be recalled. A phase that removes relationships must run before the GraphEmitSink is installed (see the parse-boundary construction in pipeline.ts). (abhigyanpatwari/GitNexus)
- Invalid execution transition for intent {intent_id}: {current_status} -> {} (nautechsystems/nautilus_trader)
- Execution intent {intent_id} cannot mark {event} emitted (nautechsystems/nautilus_trader)
- Trying to acquire a lease on a resource which is in the wrong state: status must be "%s", actually "%s". (phacility/phabricator)
- ${message} (paperclipai/paperclip)
- User token not found (ruvnet/ruflo)
- Cart is already charging! (phacility/phabricator)
- manufacturing::system.manufacturing-manager.unplan-order.work-orders-already-started (aureuserp/aureuserp)
- error-invalid-operation-status (RocketChat/Rocket.Chat)
- Invalid execution transition for intent {intent_id}: {current_status} -> replaced (nautechsystems/nautilus_trader)
- Workflow cannot be resumed (ruvnet/ruflo)
- Blueprint "%s" (of type "%s") is not properly implemented: %s must actually allocate the resource it returns. (phacility/phabricator)
- Trying to activate a lease which has the wrong status: status must be "%s", actually "%s". (phacility/phabricator)
- Profiling data cannot be updated while profiling is in progress. (facebook/react)
- NOT_APPLIED: Application #${appNum} is not Applied (status: "${row.status.trim()}"); use --force to seed anyway (santifer/career-ops)
- Cart is not charging yet! (phacility/phabricator)
- Cannot update product-level rich content while in per-variant mode. Set has_same_rich_content_for_all_variants to true first, or use the variant endpoint to update per-variant content. (antiwork/gumroad)
- Cannot switch to shared content: both product-level and variant-level content exist. Remove one side first, or send replacement rich_content in the same request. (antiwork/gumroad)
- illegal observation generation job transition from ${current.status} to ${nextStatus} (thedotmack/claude-mem)
…and 96 more across the corpus — use search.
Honest provenance: generated on 2026-08-21 from AI-assisted analysis of the linked records. See how records are made.