**Author**: Jesse Chejieh
**Related Issues**: [#10438](https://github.com/paritytech/polkadot-sdk/issues/10438), [#9966](https://github.com/paritytech/polkadot-sdk/issues/9966), [#3672](https://github.com/paritytech/polkadot-sdk/issues/3672)
## 1. Executive Summary
**Goal**: This document outlines the implementation design for a general purpose ***permissionless scheduler*** / ***cron*** that allows any account (EOA, contract, or governance) to schedule arbitrary `RuntimeCall` execution at future timestamps (one-time or recurring), with execution costs pre-funded via a deposit mechanism. Authoring a new age of of on-chain automation and "agentic" contracts.
**Architectural Decision**: Build `pallet_cron` on top of the FRAME Task System as it's sole execution engine. An infrastructure providing safe, panic-resistant, weight-metered executions.
**Scope**: The pallet is general-purpose, exposing dispatchables for native accounts and a precompile for EVM contracts. It uses `pallet_timestamp` to accommodate asynchronous backing and dynamic block times.
**Flow**:
```mermaid
graph TD
A[Native Account] -->|schedule call| D[pallet_cron::schedule]
B[EVM Contract] -->|call| E[CronPrecompile]
E -->|dispatch| D
D -->|store| F[ScheduledCalls Storage]
D -->|reserve| G[Deposit Account<br/>fungible::MutateHold]
H[Offchain Worker] -->|scan due tasks| I[enumerate_due_tasks]
I -->|submit unsigned| J[frame_system::do_task]
J -->|execute| K[Task System]
K -->|condition check| L[task_condition]
K -->|execute| M[execute_scheduled_call]
M -->|dispatch| N[Target RuntimeCall<br/>with Scheduler Origin]
M -->|deduct cost| G
style D fill:#f9f,stroke:#333,stroke-width:2px
style K fill:#bbf,stroke:#333,stroke-width:2px
```
## 2. Pallet Design
### 2.1 Configuration Trait
```rust
#[pallet::config]
pub trait Config: frame_system::Config {
/// Fungible asset for deposits and execution payments (uses Holds).
type Currency: fungible::MutateHold<Self::AccountId, Reason = Self::RuntimeHoldReason>;
type RuntimeHoldReason: From<HoldReason>;
/// Time provider for timestamps.
type Time: Get<<Self as pallet_timestamp::Config>::Moment>;
/// Maximum tasks per account.
#[pallet::constant]
type MaxTasksPerAccount: Get<u32>;
/// Time bucket in seconds (e.g., 1 for per-second, 60 for per-minute).
/// Per-minute recommended for Asset-Hub (2s blocks), per-second for general chains.
#[pallet::constant]
type Bucket: Get<u64>;
/// Weight information.
type WeightInfo: WeightInfo;
}
```
### 2.2 Storage Items
```rust
/// Core storage for scheduled tasks.
#[pallet::storage]
pub type ScheduledCalls<T: Config> = StorageMap<
_,
Blake2_128Concat,
u64, // task_id
ScheduledCallDetails<T::AccountId, T::RuntimeCall, BalanceOf<T>>,
OptionQuery,
>;
/// Auto-incrementing task ID.
#[pallet::storage]
pub type NextTaskId<T: Config> = StorageValue<_, u64, ValueQuery>;
#[pallet::storage]
pub type Agenda<T: Config> = StorageDoubleMap<
_,
Twox64Concat,
u64,
Twox64Concat,
u64,
(),
ValueQuery,
>;
/// Reverse index: scheduler account -> their task IDs.
#[pallet::storage]
pub type TasksByScheduler<T: Config> = StorageMap<
_,
Blake2_128Concat,
T::AccountId,
BoundedVec<u64, T::MaxTasksPerAccount>,
ValueQuery,
>;
```
### 2.3 Data Structures
```rust
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub struct ScheduledCallDetails<AccountId, RuntimeCall, Balance> {
pub scheduler: AccountId,
pub call: RuntimeCall,
pub schedule: Schedule,
pub deposit: Balance,
pub status: TaskStatus,
pub executions_remaining: Option<u32>,
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum Schedule {
OneTime { execute_at: u64 },
Recurring {
start_at: u64,
interval: u64,
max_executions: Option<u32>,
},
}
#[derive(Clone, Encode, Decode, Eq, PartialEq, RuntimeDebug, TypeInfo, MaxEncodedLen)]
pub enum TaskStatus {
Active,
Completed,
Cancelled,
}
impl Schedule {
pub fn next_execution(&self, now: u64) -> Option<u64> {
match self {
Schedule::OneTime { execute_at } if *execute_at > now => Some(*execute_at),
Schedule::Recurring { start_at, interval, .. } if *start_at > now => Some(*start_at),
Schedule::Recurring { start_at, interval, .. } => {
let elapsed = now.saturating_sub(*start_at);
let next = ((elapsed / interval) + 1) * interval;
Some(start_at.saturating_add(next))
}
_ => None,
}
}
}
```
### 2.4 Dispatchables
```rust
#[pallet::call]
impl<T: Config> Pallet<T> {
/// Schedule a call, restrict calls to origins that
/// have no call filters.
#[pallet::weight(...)]
pub fn schedule(
origin: OriginFor<T>,
call: Box<<T as Config>::RuntimeCall>,
schedule: Schedule,
#[pallet::compact] deposit: BalanceOf<T>,
) -> DispatchResult
/// Cancel a scheduled task and unreserve deposit.
#[pallet::weight(...)]
pub fn cancel(
origin: OriginFor<T>,
task_id: u64,
) -> DispatchResult
/// Top up deposit for a paused task.
#[pallet::weight(...)]
pub fn top_up(
origin: OriginFor<T>,
task_id: u64,
#[pallet::compact] additional: BalanceOf<T>,
) -> DispatchResult
}
```
### 2.5 Task Execution
```rust
#[pallet::tasks_experimental]
impl<T: Config> Pallet<T> {
#[pallet::task_index(0)]
#[pallet::task_list(|| { /* ... enumeration logic ... */ })]
#[pallet::task_condition(|task_id| { /* ... validation ... */ })]
pub fn execute_scheduled_call(task_id: u64) -> DispatchResult
}
impl<T: Config> Pallet<T> {
fn calculate_execution_cost(call: &T::RuntimeCall) -> BalanceOf<T> {
let info = call.get_dispatch_info();
let len = call.encoded_size() as u32;
// Option A: Use transaction_payment
// Option B: Manual calculation using WeightToFee
}
}
```
### 2.6 Trigger
```rust
#[pallet::hooks]
impl<T: Config> Hooks<BlockNumberFor<T>> for Pallet<T> {
fn offchain_worker(_block_number: BlockNumberFor<T>) {
// ... iterate Agenda ...
let task = Task::<T>::ExecuteScheduledCall { task_id };
// "Cron" must match the name in construct_runtime!
let runtime_task = RuntimeTask::Cron(task);
let call = frame_system::Call::<T>::do_task { task: runtime_task };
SubmitTransaction::<T, _>::submit_unsigned_transaction(call.into());
}
}
```
## 3. Testing Strategy
**Unit Tests**:
- Schedule, cancel, verify storage state.
- Verify holds, burns, and releases.
- Verify multiple executions update executions_remaining correctly.
- Ensure filtered calls fail at schedule.
**Fuzzing**:
- Randomized schedules and timestamps to verify agenda consistency.
## 4. Open Questions & Future Work
1. **Task Prioritization**: Should users pay a priority fee for earlier execution within a time slot?
2. **Preimage Handling**: For large calls, integrate pallet_preimage to store call hash instead of full encoding.