> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Schedule a contract call

> Authorize a future contract call, fund its execution, and verify the result

> [!NOTE]
>
> **What you'll be able to do**
>
> * Schedule a contract call for a future time or block height.
> * Verify the target contract's execution result.
> * Cancel pending entries and withdraw available escrow.
> * Add an oracle condition or encrypt the call's payload.

> [!WARNING]
>
> The scheduler is [not on Metal Testnet yet](https://docs.metalntwx.com/reference/contract-addresses.md#contracts-not-yet-on-metal-testnet).
> Follow this guide on a network that has code at its address.

Metal's scheduler at `0x00000000000000000000000000000000000000fe` runs contract
calls at a future time or block height, using tokens deposited in escrow to pay
for execution. Its API calls the account that deposits tokens and schedules the
call the `payer`. Scheduling and execution produce separate transaction
receipts, so record both and verify the target contract's state before marking
the action complete.

## Check deployment and fund the signing account

Check for code at the scheduler address. Read the
scheduler's `gasToken`, `minGasPrice`, `gasPerBlock`, `maxHorizonSlots`,
`maxLateSlots`, and `cancelRefundBps` to use this network's token and scheduling
limits.

Use the
[token and spending approval procedure](https://docs.metalntwx.com/reference/assets-and-payments/mip20.md#approve-a-spender)
to verify the scheduler's gas token. Later, you will approve and deposit the
amount needed for the call. Keep enough of the signing account's balance in the
gas token outside escrow to pay the transaction fees for approval, scheduling,
cancellation, and withdrawal. Use
[fee estimates](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md) to calculate those costs.

## Authorize the target action

The target contract sees the scheduler as `msg.sender`, and any account can
schedule a call to it. The target therefore needs to check which actions its
owner has authorized. `ScheduledValue` lets its owner authorize one value change
per action ID, with an earliest execution time or height. Each authorization can
be used once.

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract ScheduledValue {
    address constant SCHEDULER = 0x00000000000000000000000000000000000000fe;
    address public immutable owner = msg.sender;
    uint256 public value;
    struct Action {
        uint256 value;
        uint64 notBeforeTime;
        uint64 notBeforeHeight;
        bool authorized;
        bool used;
    }
    mapping(bytes32 => Action) public actions;
    event Applied(bytes32 indexed actionId, uint256 value);

    function authorize(bytes32 id, uint256 nextValue, uint64 time, uint64 height) external {
        require(msg.sender == owner, "owner");
        require(!actions[id].authorized && !actions[id].used, "existing action");
        require((time == 0) != (height == 0), "trigger");
        actions[id] = Action(nextValue, time, height, true, false);
    }

    function revoke(bytes32 id) external {
        require(msg.sender == owner, "owner");
        actions[id].authorized = false;
        actions[id].used = true;
    }

    function run(bytes32 id) external {
        require(msg.sender == SCHEDULER, "scheduler");
        Action storage action = actions[id];
        require(action.authorized && !action.used, "authorization");
        require(block.timestamp >= action.notBeforeTime, "time");
        require(block.number >= action.notBeforeHeight, "height");
        action.used = true;
        value = action.value;
        emit Applied(id, value);
    }
}
```

[Deploy the target](https://docs.metalntwx.com/build/contracts/deploy-an-application-contract.md), then choose a
unique `bytes32` action ID and the value to set. You will authorize them after
choosing the execution time or height below.

The target enforces the stored value and earliest execution point even if
another account schedules the same action. Enforce any additional application
rules, such as an expiry time, in the target too.

## Encode a single execution

The code below prepares a call to `run(actionId)` for two minutes after the
current block's timestamp and calculates the required deposit. Use the deployed
contract address as `target` and your chosen `actionId`. See
[Scheduler reference](https://docs.metalntwx.com/reference/contracts-and-automation/scheduler.md) for the full parameter
definitions.

```typescript theme={null}
import { encodeAbiParameters, encodeFunctionData, parseAbi, parseAbiParameters,
  size, zeroAddress, zeroHash } from 'viem'

const scheduler = '0x00000000000000000000000000000000000000fe' as const
const schedulerAbi = parseAbi([
  'struct Call { address target; uint64 gasLimit; bytes data; }',
  'struct Dependency { uint64 id; bool finished; }',
  'struct Condition { bytes32 item; uint8 kind; uint256 operand; }',
  'struct Params { Call[] calls; uint64 slot; uint64 height; uint64 gasLimit; uint64 gasPrice; uint8 class; bool lateExecute; uint8 maxRetries; uint32 backoffSlots; uint64 periodSlots; uint32 remaining; bool pauseOnFail; address reserveToken; uint96 reserveAmount; Dependency[] dependsOn; bytes ciphertext; bytes32 commitment; Condition when; }',
  'function gasToken() view returns (address)',
  'function minGasPrice() view returns (uint64)',
  'function gasPerBlock() view returns (uint64)',
  'function maxHorizonSlots() view returns (uint64)',
  'function maxLateSlots() view returns (uint64)',
  'function cancelRefundBps() view returns (uint16)',
  'function cost(uint64 gasLimit, uint256 dataLen, uint256 dependencies, bool recurring, bool reserved) pure returns (uint64)',
  'function balance(address payer) view returns (uint256)',
  'function deposit(uint256 amount)',
  'function withdraw(uint256 amount)',
  'function schedule(Params params) returns (uint64)',
  'function cancel(uint64 id)',
  'function expire(uint64 id)',
  'function status(uint64 id) view returns (uint8)',
  'event Deposited(address indexed payer, uint256 amount)',
  'event Withdrawn(address indexed payer, uint256 amount)',
  'event Scheduled(uint64 indexed id, address indexed payer, uint64 slot, uint64 height, uint8 class)',
  'event Executed(uint64 indexed id, uint8 attempt)',
  'event ExecutionFailed(uint64 indexed id, uint8 attempt, bool willRetry)',
  'event Cancelled(uint64 indexed id, uint256 refund)',
  'event Expired(uint64 indexed id, uint256 refund)',
])
const targetAbi = parseAbi(['function run(bytes32 id)'])
const calls = [{
  target,
  gasLimit: 100_000n,
  data: encodeFunctionData({ abi: targetAbi, functionName: 'run', args: [actionId] }),
}]
const head = await client.getBlock()
const gasPrice = await client.readContract({
  address: scheduler, abi: schedulerAbi, functionName: 'minGasPrice',
})
const params = {
  calls, slot: head.timestamp + 120n, height: 0n,
  gasLimit: 200_000n, gasPrice, class: 1,
  lateExecute: false, maxRetries: 0, backoffSlots: 0,
  periodSlots: 0n, remaining: 0, pauseOnFail: false,
  reserveToken: zeroAddress, reserveAmount: 0n, dependsOn: [],
  ciphertext: '0x' as const, commitment: zeroHash,
  when: { item: zeroHash, kind: 0, operand: 0n },
}
const encodedCalls = encodeAbiParameters(
  parseAbiParameters('(address target, uint64 gasLimit, bytes data)[]'), [calls],
)
const budget = await client.readContract({
  address: scheduler, abi: schedulerAbi, functionName: 'cost',
  args: [params.gasLimit, BigInt(size(encodedCalls)), 0n, false, false],
})
const lock = budget * gasPrice
```

From the target owner's account, call
`authorize(actionId, nextValue, params.slot, 0)`, where `nextValue` is the value
you chose. Verify that transaction's receipt and finality before scheduling, and
save the action ID, expected value, and timestamp in application storage. Choose
enough lead time for authorization and funding so that `params.slot` remains in
the future and within `maxHorizonSlots` when you submit the schedule.

To schedule by height, set `slot: 0n` and `height: head.number + 20n`, then
authorize with time zero and that height. Exactly one trigger must be nonzero.

The gas limits above cover this value-setting target. For a different action,
allow enough execution gas for the individual calls and overhead, and check that
`budget` fits `gasPerBlock`. Each execution attempt consumes the full reserved
amount (`lock`), including when the target fails or uses less gas. The
transaction that submits `schedule` pays its own fee.

Class `1` is BestEffort, so the call may wait for block capacity. Class `0`
reserves Guaranteed capacity at its slot or height, and scheduling can fail if
that capacity is full. Both classes depend on the chain producing blocks. The
parameters above disable retries and recurrence for a single execution attempt.

## Deposit and schedule

Read the available escrow with `balance(payer)`. If it is less than `lock`,
approve the scheduler to spend the difference in its gas token, then call
`deposit` for that amount. Verify the `Deposited` event and the increase in
available escrow.

Use [SDK signing](https://docs.metalntwx.com/reference/typescript-sdk.md#signmip20contract) to sign
`schedule(params)`, and save the transaction hash, signed bytes, and full
parameters before broadcasting. Follow
[receipt and finality verification](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md) to
verify the transaction, then decode the scheduler's `Scheduled` event. Check the
`payer`, trigger, and class, and save the entry's `id` from that event.

At the scheduling transaction's receipt block, check for status `1`
(`Scheduled`) and a decrease in available escrow equal to `lock`. Read at that
block because later state may already reflect execution. Save the original
trigger and `maxLateSlots` to determine when a pending entry can expire.

## Verify the actual execution

Query the scheduler's `Executed` and `ExecutionFailed` logs for the saved entry
ID from the schedule block onward. Use bounded block ranges and persist the last
checked block. Each execution is a transaction of type `0x7e` generated by the
chain, with a receipt separate from the scheduling receipt.

Use [receipt and finality verification](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md)
for the execution transaction, then check both protocol and application
evidence:

| Observation | Interpretation and action |
| - | - |
| `Executed(id, attempt)` and target `Applied(actionId, expectedValue)` | Read the target action and value at this block. Mark complete only if the expected action was consumed and value matches. |
| `ExecutionFailed(id, attempt, false)` | The target action failed and status becomes `3` (`Failed`). All calls and reserve capture in that run revert. Verify the target is unchanged. |
| `ExecutionFailed(id, attempt, true)` | A retry is pending and its execution cost has already been reserved from available escrow. Keep the application action pending. |
| Successful execution receipt without matching target evidence | Keep the action pending and investigate the scheduler events and target state. The receipt reports the scheduler transaction's result. |
| Reverted execution receipt and status still `Scheduled` | Check the reference's expiry rules to determine whether the entry can expire, then simulate `expire(id)` before submitting it. |

For this nonrecurring target, successful status is `2` (`Executed`). Follow
[settlement semantics](https://docs.metalntwx.com/reference/network-fundamentals/settlement.md) before exporting the result to
another system. Reconcile the amount charged for execution independently of the
target result.

## Cancel, expire, and withdraw

To cancel a pending entry, call `cancel(id)` from its `payer`. Verify
`Cancelled(id, refund)`, status `4` (`Cancelled`), and the transaction's receipt
and finality. Cancellation returns `floor(lock * cancelRefundBps / 10000)` to
available escrow and credits the remainder to the treasury's escrow.

Revoke the target authorization too if you want to prevent the action from
running through another entry. Another account may have scheduled the same
authorized action, and execution can happen before revocation or cancellation.
Inspect finalized target state before marking the application action cancelled.

With `lateExecute: false`, a time-triggered entry can expire once chain time is
strictly greater than its original slot plus `maxLateSlots`. Anyone can then
call `expire(id)` to release the reserved amount. Verify `Expired(id, lock)`,
status `5` (`Expired`), and the full amount returned to available escrow. Height
entries have no time deadline. See
[Scheduler reference](https://docs.metalntwx.com/reference/contracts-and-automation/scheduler.md#cancelling-expiring-and-withdrawing)
for expiry based on queue position or encrypted payloads.

Read `balance(payer)` and submit `withdraw(amount)` from the `payer` for up to
that available escrow balance. Verify `Withdrawn`, the gas token's matching
transfer from scheduler to `payer`, and the reduced escrow balance. Pending
entries keep their execution costs reserved, while completed attempts consume
those costs even when the target fails.

A withdrawal is subject to the `payer`'s
[receive policy](https://docs.metalntwx.com/reference/compliance/receive-policy.md). A refusal in `REVERT` mode rolls
back the withdrawal and preserves available escrow. In `QUARANTINE` mode, the
withdrawal consumes that escrow and transfers the tokens to ReceivePolicy
quarantine. Save the matching quarantine receipt and finalized token transfer,
then follow the receipt's recovery procedure to recover those tokens.

## Add an oracle condition

Choose a listed [oracle item](https://docs.metalntwx.com/build/data-and-signatures/use-oracle-data.md) that has been
written at least once, and verify its ID, decimals, and units before converting
the threshold. Set `when` to
`{ item: itemId, kind: 1, operand: thresholdInBaseUnits }` for a value at least
equal to the threshold, or kind `2` for a value at most equal to it. Kind `3`
fires on a write while the item is not halted. Use BestEffort class,
`height: 0n`, no recurrence, and `slot` as the earliest qualifying write
timestamp.

The condition is evaluated on later oracle writes whose timestamps are at or
after `slot`, even if the value already meets the threshold when you schedule. A
qualifying write queues the entry into a future slot when capacity permits.
While waiting, the entry remains pending until it fires, you cancel it, or the
item leaves the catalog and makes it eligible for expiry.

If your application also requires bounds on freshness, confidence, or flags,
check them in the target before acting. After execution, verify the target event
and state, and record the qualifying write alongside the execution result. If
the item never produces a qualifying write, cancel the entry and reconcile its
refund.

## Encrypt the payload

Read the [beacon](https://docs.metalntwx.com/build/data-and-signatures/read-onchain-randomness.md) group key and
encrypt the calls using a sealing implementation that follows the
[encrypted payload encoding](https://docs.metalntwx.com/reference/contracts-and-automation/scheduler.md#encrypted-payloads).
That format defines the padded calls, random salt, commitment, encrypted data
key, and associated data bound to the `payer`. The calls stay private until
their decryption key is available.

Set `calls: []`, put the encrypted bytes in `ciphertext`, and set the matching
`commitment`. Calculate `cost` using the ciphertext length. Use a slot trigger
with recurrence and reserves disabled. To combine encryption with an oracle
condition, use the condition identity specified in the same reference.

Once the key arrives and the calls run, verify the target outcome as you would
for an unencrypted entry. Malformed or mismatched plaintext fails the entry and
consumes its reserved amount without retry.

If the key is late, an encrypted entry waiting in a slot becomes eligible for
expiry when `block.timestamp > entry.key + 1 + 30`. Calling `expire` then
refunds the full reserved amount. Read `entry(id)` with the
[interface for reading entries](https://docs.metalntwx.com/reference/contracts-and-automation/scheduler.md#read-entry-state)
to obtain `key`, the queued slot. For an oracle condition, this grace period
starts after the entry leaves the watch list and enters that slot. Use `key` to
calculate the deadline because `dueSlot` retains the original earliest
qualifying write timestamp.

Validators can recover the decryption key before it is published onchain,
allowing a proposer to see the payload before other users.

## Test and recover

Compile the target and check the ABI calls, then test unauthorized calls,
repeated execution, revoked authorization, successful execution, and target
failure. A revoked action provides a failure case whose target state must remain
unchanged. Test cancellation with a separate action scheduled far enough in the
future, then withdraw the refund. Use controlled fixtures to test expiry
boundaries and classify receipts when live timing is unreliable.

Include withdrawal refusal and quarantine fixtures. If adding a reserve, test
that capture can consume it into quarantine while the entry becomes `Executed`.
Verify recipient payment separately as described in
[Reserves](https://docs.metalntwx.com/reference/contracts-and-automation/scheduler.md#reserves).

After an RPC error, look up the saved scheduling transaction hash and use the
entry ID and last checked block to resume monitoring execution. Resolve the
original submission before scheduling a replacement. Keep the parameters,
network limits, transaction hashes, finality checks, target outcomes, and
changes in escrow balance with the application action's record.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.