How Automation Works
EVM automation lets a contract be called without anyone sending a transaction. A user registers a task naming a target call and a condition gating it; once the task is active, the chain schedules that call for execution in every committed block of a running cycle, until the task ends. Task lifecycle lists the ways a task ends.
This page covers the feature model behind AutomationRegistry: cycles, configuration, fees, task validity, and the difference between a user task and a system task. That page stays a contract reference — entry points, callers, errors, events, ABI — and links back here for the concepts.
Cycles and blocks
Two clocks govern a task, and they run at different rates.
Blocks are the execution interval. Once a task is ACTIVE, the node schedules its call
in every committed block while the registry cycle is STARTED, for as long as the task
remains active and its predicate allows it. It schedules nothing during the bookkeeping that
follows a cycle’s end, or while the registry is SUSPENDED or READY. A task is also
skipped, for that block only, when the lowest gas price included in the block is above the
task’s gasPriceCap; see
Gas and Fees.
Cycles are the fee, capacity and registration window. Registration is accepted only
while the current cycle is in the STARTED state with automation and registration both
enabled. A task’s expiry is checked against the end of the cycle it registers into; its fee
cap is compared with the projected fee for the next cycle, the first one the task is charged
for. A task does not skip straight from registration to execution: it is created PENDING and
only becomes ACTIVE at the next cycle transition. A task registered mid-cycle therefore
waits for the cycle to turn before it runs at all — but once it is active, it runs every
block, not once per cycle.
Cycle states
| State | Meaning |
|---|---|
READY | Automation is available but not enabled. The automation registry holds no task, registration is refused, and the cycle does not progress |
STARTED | Automation is enabled and usable. Tasks can be registered while registration is enabled; the cycle’s end time is monitored, and when it passes the cycle index advances |
FINISHED | The active cycle has ended. The automation registry is transitioning to the next cycle, and bookkeeping for that cycle is in progress |
SUSPENDED | Governance has disabled automation. Bookkeeping runs over the registered tasks to refund and remove them, cleaning up the automation registry before it returns to READY |
At genesis the automation registry starts in STARTED with cycle index 1 if the chain’s
automation parameters enable automation, and in READY with cycle index 0 otherwise. A
configuration staged with updateConfigBuffer is adopted at the next cycle boundary: when a
cycle ends, or when automation is enabled again.
Cycle transitions
| From | To | When | Cycle index |
|---|---|---|---|
READY | STARTED | The owner enables automation | +1 |
STARTED | FINISHED | The cycle’s end time passes and at least one task is registered | unchanged |
STARTED | STARTED | The cycle’s end time passes and no task is registered | +1 |
STARTED | SUSPENDED | The owner disables automation while at least one task is registered | unchanged |
STARTED | READY | The owner disables automation while no task is registered | unchanged |
FINISHED | STARTED | Bookkeeping has processed every task | +1 |
FINISHED | SUSPENDED | The owner disables automation after the cycle ended but before any bookkeeping record for the next cycle was processed | unchanged |
SUSPENDED | READY | Bookkeeping has refunded and removed every task | unchanged |
SUSPENDED | STARTED | Bookkeeping has finished, and the owner enabled automation again during the suspension | +1 |
Every transition emits AutomationCycleEvent, carrying the cycle index, the new state and the
previous state.
What drives a transition
monitorCycleEnd() runs every block once it is registered in BlockMeta.
Once block.timestamp passes the cycle’s end time, it moves the cycle from STARTED to
FINISHED, or straight into a new STARTED cycle if no task is registered.
monitorCycleEnd() only detects the end of a cycle; it does no task bookkeeping itself.
Genesis does not register monitorCycleEnd(). A foundation-wallet action registers it
afterwards, and until then no cycle ends, so the cycle stays STARTED past its end time and every task stays PENDING.
processTasks(cycleIndex, taskIndexes) is the VM signer’s bookkeeping call, driven by the
node once a transition is detected, and is called repeatedly in batches until every task has
been processed. It does not execute a task’s registered action. In the FINISHED state it
activates PENDING tasks, charges each user task its fee for the cycle being entered, and
drops the tasks that cannot continue; once every task is processed, a new STARTED cycle
begins. In the SUSPENDED state it refunds and removes every task without charging; once
done, the automation registry moves to READY, or to STARTED if automation was enabled
again meanwhile.
The owner’s enableAutomation() and disableAutomation() drive the remaining transitions;
see Enabling and disabling automation.
removeRegisteredTask(cycleIndex, taskIndex, reason, details) is a separate VM-signer call
the node uses to drop a single task mid-cycle: one it finds structurally invalid, or one a new
epoch’s EVM gas config no longer admits. See Removal by the runtime.
None of monitorCycleEnd, processTasks or removeRegisteredTask can be invoked by any
account, because no transaction can be sent from the VM signer’s reserved address; see
SupraEVM Precompiles. They are described here
so they can be recognised in traces and in RPC responses.
Enabling and disabling automation
Only the owner — the foundation wallet — calls enableAutomation() and
disableAutomation(). Each reverts if automation is already in the requested state
(AlreadyEnabled, AlreadyDisabled). When a call takes effect depends on the cycle state:
| Cycle state | disableAutomation() | enableAutomation() |
|---|---|---|
READY | Reverts AlreadyDisabled | Immediate: a new STARTED cycle begins, index +1, with any staged configuration adopted |
STARTED | Immediate: SUSPENDED if any task is registered, otherwise READY | Reverts AlreadyEnabled |
FINISHED, before any bookkeeping record is processed | Immediate: SUSPENDED | Reverts AlreadyEnabled |
FINISHED, bookkeeping under way | Deferred: only the flag changes. See below | If automation was disabled during this bookkeeping, re-enables it and the transition completes into STARTED as normal; otherwise reverts AlreadyEnabled |
SUSPENDED | Reverts AlreadyDisabled | Deferred: only the flag changes, and when the suspension bookkeeping finishes the automation registry moves to STARTED, index +1, instead of READY |
Disabling while bookkeeping is under way. The transition already in progress completes
first: when the last task is processed the automation registry enters the next cycle —
STARTED, index +1 — and, in the same transaction, moves on to SUSPENDED if tasks remain or
to READY if none do. Two AutomationCycleEvents are emitted, and an indexer following them
sees the intermediate cycle.
What a suspension refunds. Every user task is refunded its whole deposit, with no
PENDING penalty, and an ACTIVE task also its fee for the remainder of the current cycle.
When the suspension follows the end of a cycle before any task was charged for the next one,
only the deposits are refunded, since no cycle fee was taken.
Disabling mid-cycle reverts with InvalidRegistryState if the cycle’s end time has already
passed but monitorCycleEnd() has not yet ended it in that block.
Configuration
Every parameter lives in the registry’s Config struct and is read through getConfig().
Four size caps live outside Config, read through getDataLengthCaps().
| Parameter | Consumed by |
|---|---|
registryMaxGasCap | Denominator of the per-cycle fee and of congestion usage; ceiling on gas committable by user tasks for the next cycle |
sysRegistryMaxGasCap | Same ceiling for system tasks. Not a fee input — system tasks pay nothing |
automationBaseFeeWeiPerSec | Floor of the per-second automation fee |
flatRegistrationFeeWei | One-off charge taken at registration |
congestionBaseFeeWeiPerSec | Scales the congestion surcharge. 0 disables congestion entirely |
congestionThresholdPercentage | Occupancy percentage above which the surcharge applies. 100 disables it |
congestionExponent | Exponent of the surcharge curve |
maxCongestionExponent | Governance ceiling on congestionExponent |
taskDurationCapSecs | Maximum lifetime of a user task |
sysTaskDurationCapSecs | Maximum lifetime of a system task |
cycleDurationSecs | Length of a cycle. Also caps the duration charged to a task within one cycle |
taskCapacity | Maximum concurrent user tasks |
sysTaskCapacity | Maximum concurrent system tasks |
maxPayloadLength / maxPredicateLength | Size caps on _payloadTx and _predicate. Multiples of 32, since both values must be whole ABI encodings |
maxAuxDataLength / maxAuxDataEntries | Size caps on _auxData |
Values at genesis
Genesis derives the registry’s configuration from the chain’s Move automation configuration, so that both VMs’ automation is governed by one set of parameters:
| EVM parameter | Derived from the Move parameter |
|---|---|
automationBaseFeeWeiPerSec, congestionBaseFeeWeiPerSec, flatRegistrationFeeWei | The matching fee in quants, multiplied by 10¹⁰ |
registryMaxGasCap, sysRegistryMaxGasCap | The matching gas cap, multiplied by 3,000 |
taskDurationCapSecs, sysTaskDurationCapSecs, cycleDurationSecs, congestionThresholdPercentage, congestionExponent | Copied unchanged |
maxCongestionExponent | Set to congestionExponent |
taskCapacity, sysTaskCapacity | Copied unchanged when their sum is at most 200; otherwise each is capped, at 160 and 40 respectively |
One quant is 10¹⁰ wei, so each EVM fee equals the matching Move fee in SUPRA. The gas caps are
multiplied by 3,000 because at the conversion of a Move gas price into an EVM one, described in
The gas price floor, 3,000 EVM gas cost the same as
one Move gas unit. A task therefore takes the same share of either registry for the same amount
of work, and pays the same fee in SUPRA. maxPayloadLength and maxPredicateLength start at
4,096 and 2,048 bytes; maxAuxDataLength and maxAuxDataEntries start at 0, which disables
_auxData.
Governance can change any of these values after genesis, so read the live configuration from
the chain rather than relying on a printed figure. supra_devnet is the endpoint name defined in
Foundry; a wallet-integration URL can be passed instead, and AUTOMATION_REGISTRY
is the address from the contract registry:
cast call "$AUTOMATION_REGISTRY" \
"getConfig()((uint128,uint128,uint128,uint128,uint128,uint64,uint64,uint64,uint16,uint16,uint8,uint8,uint8))" \
--rpc-url supra_devnet
cast call "$AUTOMATION_REGISTRY" "getDataLengthCaps()(uint16,uint16,uint16,uint16)" \
--rpc-url supra_devnetgetConfig() returns the fields in the order of the Config struct: registryMaxGasCap,
sysRegistryMaxGasCap, automationBaseFeeWeiPerSec, flatRegistrationFeeWei,
congestionBaseFeeWeiPerSec, taskDurationCapSecs, sysTaskDurationCapSecs,
cycleDurationSecs, taskCapacity, sysTaskCapacity, congestionThresholdPercentage,
congestionExponent, maxCongestionExponent. $AUTOMATION_REGISTRY is the registry’s
address, found as in
Getting Started.
Devnet’s Move automation configuration produces the following registry configuration at Devnet’s next genesis. A registry deployed by an earlier genesis keeps the values that genesis gave it until governance changes them.
| Parameter | Devnet value |
|---|---|
registryMaxGasCap | 240,000,000 |
sysRegistryMaxGasCap | 60,000,000 |
automationBaseFeeWeiPerSec | 4,000,000,000,000,000 wei/s (0.004 SUPRA/s) |
flatRegistrationFeeWei | 500,000,000,000,000,000 wei (0.5 SUPRA) |
congestionBaseFeeWeiPerSec | 4,000,000,000,000,000 wei/s |
congestionThresholdPercentage | 50% |
congestionExponent / maxCongestionExponent | 6 / 6 |
taskDurationCapSecs | 172,800 seconds (2 days) |
sysTaskDurationCapSecs | 2,626,560 seconds (30.4 days) |
cycleDurationSecs | 600 seconds |
taskCapacity / sysTaskCapacity | 160 / 40 |
maxPayloadLength / maxPredicateLength | 4,096 / 2,048 bytes |
maxAuxDataLength / maxAuxDataEntries | 0 / 0 — _auxData is disabled |
At these values a task with a _maxGasAmount of 100,000 costs 10¹⁵ wei (0.001 SUPRA) per cycle
below the congestion threshold, and 5 × 10¹⁷ wei (0.5 SUPRA) to register, plus the fee cap it
deposits. estimateAutomationFee returns the current per-cycle figure for a given gas amount.
Buffered versus immediate
updateConfigBuffer stages a full configuration change, adopted only at the next cycle
transition, so an in-progress cycle’s rules cannot move underneath registered tasks. Two
fields inside it escape the buffer and take effect immediately —
nextCycleRegistryMaxGasCap and nextCycleSysRegistryMaxGasCap — so a lowered gas cap
gates new registrations within the current cycle rather than waiting a full cycle to bite.
updateDataLengthCaps is always immediate. The size caps only gate new registrations, and
registration is already blocked outside the STARTED state, so there is no mid-cycle
fairness reason to defer them.
The fee model
Registering a user task charges flatRegistrationFeeWei + automationFeeCapForCycle in
WSUPRA, the token of the WrappedSupra contract (contract registry key WrappedSupra), taken
by transferFrom; see
Predeployed Contracts. The cap becomes
the task’s locked deposit, and is
checked against a fee projection at registration — too low a cap fails registration outright
with InsufficientFeeCapForCycle, naming the required amount. Registering a system task
charges nothing.
The per-second automation fee is a base rate plus a congestion surcharge:
automationFeePerSec = automationBaseFeeWeiPerSec + congestionFee. The surcharge is zero
below congestionThresholdPercentage occupancy, and above it rises with the exponent
congestionExponent, scaled by congestionBaseFeeWeiPerSec.
A task’s fee for one cycle is automationFeePerSec × chargedDuration × maxGasAmount / registryMaxGasCap, where chargedDuration is the task’s remaining lifetime clamped to the
cycle length. A task still PENDING at a cycle transition is charged the full cycle
interval regardless of a shorter registered lifetime.
At a cycle transition each task is handled by the first of these that applies:
| Condition | Result |
|---|---|
| The task is cancelled or has expired | Dropped; a user task’s deposit refunded in full |
| The epoch’s EVM gas config no longer admits the task | Dropped before it is charged; a user task’s deposit refunded in full; TaskRemovedByGasConfigUpdate |
| The task is a system task | Kept; never charged |
The fee exceeds the task’s automationFeeCapForCycle | Dropped; deposit refunded in full; TaskCancelledCapacitySurpassed |
| The owner’s WSUPRA balance or allowance is below the fee | Dropped; deposit unlocked but not refunded; TaskCancelledInsufficientBalanceAllowance |
| Otherwise | Fee taken; TaskCycleFeeWithdraw |
RemovedTasks lists every task dropped at a transition, whichever row dropped it.
A task is admitted by the gas config while its maxGasAmount is within the chain’s
per-transaction gas cap and, for a user task, its gasPriceCap is at least the chain’s
minimum gas price. The rules and their reasoning are in
Gas and Fees.
System tasks are never charged and never refunded — every fee and refund path is conditioned on the task being a user task.
These are the WSUPRA charges. A user task’s action also pays gas at execution, from the owner’s native balance; see What an execution looks like.
Task lifecycle
Every task is created PENDING. It becomes ACTIVE only at the next cycle transition, so a
task registered mid-cycle waits for the cycle to turn before its first execution. Once
active, it is scheduled in every committed block while the cycle is STARTED.
| Ending | Trigger | Event | Refund (user task) |
|---|---|---|---|
Cancelled while PENDING | cancelTasks / cancelSystemTasks; removed immediately | TasksCancelled | Half the deposit |
Cancelled while ACTIVE | Same call; marked CANCELLED, keeps executing until the cycle ends, removed at the next cycle transition | TasksCancelled | Full deposit, at that transition |
Stopped while PENDING | stopTasks / stopSystemTasks; removed immediately | TasksStopped | Half the deposit, no cycle-fee refund |
Stopped while ACTIVE or CANCELLED | Same call; removed immediately | TasksStopped | Full deposit, plus half the residual cycle fee |
| Expired | Removed at a cycle transition | RemovedTasks | Full deposit |
| Fee cap too low | Cycle transition charging | TaskCancelledCapacitySurpassed | Full deposit |
| Balance or allowance short | Cycle transition charging | TaskCancelledInsufficientBalanceAllowance | Deposit unlocked, not refunded |
| Found invalid by the runtime | removeRegisteredTask with reason ERROR | TaskRemovedBySystem | Full deposit plus half the residual cycle fee; half the deposit if PENDING |
| No longer admitted by the gas config, mid-cycle | removeRegisteredTask with reason GAS_CONFIG_UPDATE | TaskRemovedByGasConfigUpdate | Full deposit plus the whole residual cycle fee |
| No longer admitted by the gas config, at a transition | Cycle transition, before charging | TaskRemovedByGasConfigUpdate | Full deposit |
The half-deposit penalty attaches to the PENDING state specifically, on the cancel, stop
and runtime-removal paths — not to cancellation as a whole. An ACTIVE or already-CANCELLED
task refunds its deposit in full on each of those paths, and stopTasks and runtime removal
also refund half of whatever cycle fee was left unused. A removal for a gas-config change
carries no penalty: it refunds the whole deposit and the whole residual cycle fee. In every
case the flat registration fee is not refunded.
Cancelling versus stopping
The two calls differ in when an active task stops running:
- Cancelling a
PENDINGtask removes it from the automation registry immediately. It never runs. - Cancelling an
ACTIVEtask does not remove it. Its state changes fromACTIVEtoCANCELLED, it keeps being scheduled for execution in every block until the current cycle ends, and it is removed at the next cycle transition. - Stopping a task removes it immediately, whatever its task state. An active task is no longer scheduled from the next block.
The node fixes the set of tasks it schedules at the start of each cycle and, in every block, only drops from it the tasks that are no longer active; stopping makes a task inactive, cancelling leaves it active until the cycle ends. To halt an active task at once, stop it.
A batch passed to cancelTasks, cancelSystemTasks, stopTasks or stopSystemTasks skips
any index that no longer exists rather than reverting the whole call, so one stale entry
does not block the rest. All four require automation enabled and the cycle STARTED; in any
other cycle state they revert, whatever the task’s own state.
Removal by the runtime
removeRegisteredTask is how the node drops a single task mid-cycle. Its reason argument is
a TaskRemovalReason:
GAS_CONFIG_UPDATE— the EVM gas config that took effect at an epoch boundary no longer admits the task. The task is no longer scheduled from the first block of that epoch, and is removed when the removal record executes, with the full refund described above.TaskRemovedByGasConfigUpdatereports the task’s bounds next to the new config’s. The sequence, including how pending tasks are handled, is in Gas and Fees.ERROR— the task is structurally broken, rather than having failed a normal check.TaskRemovedBySystemcarries adetailsstring naming the fault:
| Details | Cause |
|---|---|
MalformedPayload | payloadTx failed to decode |
MalformedPredicate | predicate failed to decode, or returned a non-boolean at execution |
PredicateExecutionFailed | The predicate reverted, halted, attempted a state change, or was not a valid transaction |
UnexecutableGasAmount | maxGasAmount does not fit the 64-bit gas limit of the transaction built for the task |
The details string is the variant name followed by its payload, for example
PredicateExecutionFailed("Revert { gas_used: ..., output: ... }"). Match on the prefix, not
on equality.
UST versus GST
A task is either user-submitted (UST) or governance-submitted (GST). GST exists so
protocol-level automation — upkeep registered by governance rather than an end user — runs
without holding WSUPRA or competing with users for the user task budget. Registering a GST
is a governance mechanism, not something users apply for; applications use UST tasks.
| UST | GST | |
|---|---|---|
| Registered by | Any account without code, via register | Authorized accounts without code, via registerSystemTask |
| Authorization | — | Granted or revoked by the owner through grantAuthorization / revokeAuthorization |
| Registration fee | flatRegistrationFeeWei + automationFeeCapForCycle | None |
| Per-cycle charge | Yes | Never |
| Gasless at execution | No — the owner’s native balance pays gas | Yes |
| Capacity | taskCapacity | sysTaskCapacity |
| Gas cap | registryMaxGasCap | sysRegistryMaxGasCap |
| Duration cap | taskDurationCapSecs | sysTaskDurationCapSecs |
maxGasAmount bound | At most the chain’s per-transaction gas cap | Same |
gasPriceCap bound | At least the chain’s minimum gas price | Exempt — a system task runs at a gas price of 0 |
_priority | Ignored — always the task index | Honoured if nonzero, else the task index |
| Cancel / stop | cancelTasks, stopTasks | cancelSystemTasks, stopSystemTasks |
| Refunds | Yes | Never |
A task runs with its owner as the transaction sender, and a sender with code (other than an EIP-7702 delegation) is refused. A task registered by a contract, including a system task registered by a multisig authorized submitter, is therefore removed at its first predicate evaluation. Register from an account without code.
The owner of a task calling the UST function on a GST task, or the reverse, reverts with
UnsupportedTaskOperation. A caller that is not the owner reverts with UnauthorizedAccount
instead.
Execution within a block is ordered by type first, then by priority ascending: every UST
task runs before any GST task, and within each group the lower priority value runs
first.
The predicate
Every task carries a predicate. It is validated at registration and evaluated again before each execution.
At registration, the predicate is size-capped, required to be a whole number of 32-byte
words, decoded as (address, bytes), and staticcalled under the task’s _maxGasAmount; it
must return exactly 32 bytes holding 0 or 1. _maxGasAmount is checked against the
chain’s per-transaction gas cap before that call, so an oversized task is refused before it
can run anything. A predicate is mandatory: an empty bytes value passes the size and length
checks, then fails to decode, so registration without one is rejected rather than accepted as
unconditional.
At execution, the predicate runs as a read-only transaction and the action runs as a
second transaction, and both draw on the task’s _maxGasAmount. The predicate’s gas used,
including its intrinsic gas (21,000 plus calldata), is deducted from _maxGasAmount, and the
action then pays its own intrinsic gas out of the remainder. An expensive predicate leaves
less gas for the action it gates. The registration-time staticcall does not count intrinsic
gas, so a _maxGasAmount just above the predicate’s cost can pass registration and leave too
little for the action to start; that execution is discarded and the task stays registered.
| Predicate outcome | Effect |
|---|---|
Returns true | The action runs this block |
Returns false | The task is skipped for this block only, and stays registered |
| Reverts or halts | The task is removed — PredicateExecutionFailed |
| Attempts a state change | The task is removed — PredicateExecutionFailed |
| Is not a valid transaction (for example, gas below intrinsic) | The task is removed — PredicateExecutionFailed |
| Returns a non-boolean value | The task is removed — MalformedPredicate |
The predicate’s outcome decides the task’s fate; the action’s does not. An action that
reverts, halts or is discarded is recorded as the automated transaction’s output, and the
task stays registered. A user task whose owner’s native balance is below
gas_limit × max_fee_per_gas + value for the action is discarded in this way in each block
and is not removed.
What an execution looks like
The predicate and the action are both transactions sent by the task owner:
from,msg.senderandtx.originare the task owner. The registration-timestaticcalldiffers: theremsg.senderinside the predicate is the registry, so a predicate that testsmsg.sendersees a different value at registration than at execution.nonceis the task index. The owner’s account nonce is neither checked nor advanced, so(from, nonce)of an automated transaction overlaps the owner’s own nonce space. To tell automated from user transactions, usesupraTxKindon the Ethereum API ortxn_typeon the REST API; see Automated Transactions over RPC.- For a user task,
max_fee_per_gasis the lowest gas price included in the block and the gas used is paid from the owner’s native balance. For a system task it is 0. - The priority fee is 0. Native value in
_payloadTxis debited from the owner’s native balance.
Within a block, automated transactions run after user transactions.
Related
- AutomationRegistry — the contract reference: entry points, callers, errors, events, and the ABI.
- Automated Transactions over RPC — observing task executions and cycle bookkeeping through the RPC APIs.
- BlockMeta — runs
monitorCycleEnd()once per block, once it is registered. - Gas and Fees — how the chain’s EVM gas config bounds automation tasks, and what happens to a task when it changes.
- Predeployed Contracts — Wrapped SUPRA, the token automation fees are paid in.