# Introduction
stat-driven automation is your runtime's new autonomous nervous system. Chains should self-execute predefined logic autonomously based on state changes.
This tutorial demonstrates how to implement automated tasks in your Substrate-based runtime using the experimental `pallet::task` API to create permissionless workflow triggers.
### Objective
By completing this tutorial, you will:
- Build a Substrate pallet that automatically executes logic based on state changes.
- Understand when to use On-chain automation vs. Off-chain script.
#### Out to Scope
- Primer on FRAME.
- Off-chain worker in depth.
#### What is the Task API?
The Substrate Task API is a framework for defining state-triggered automation directly within your runtime logic. Unlike traditional smart contracts that require explicit user invocation, tasks automatically execute when predefined storage conditions are met, using a combination of:
```rust
#[pallet::tasks_experimental] // Core automation marker
#[pallet::task_list(...)] // Storage-derived candidates
#[pallet::task_condition(...)] // Execution guardrails
#[pallet::task_weight(...)] // Computation tracking
#[pallet::task_index(...)] // Task Unique identifier
```
### Key Characteristics
| Feature | Technical Implementation | Purpose |
|---------|-------------------------|---------|
| Autonomous Execution | Unsigned Transactions (`CreateInherent`) | Avoid gas fees for system-critical ops. |
| Conditional Logic | Boolean Guards in `task_condition` | Prevent invalid state transitions and spam transactions. |
| Weight Control | Benchmark-derived `task_weight` | Required for computation tracking. |
### Architectural Flow
```mermaid
graph TD
A[Off-chain Worker] --> B[Scans Storage]
B --> C[Task Candidates]
C --> D{Meets Conditions?}
D -->|Yes| E[Create Bare Tx]
D -->|No| F[Discard Task]
E --> G[Tx Pool]
G --> H[Block Inclusion]
classDef greenBox fill:#9f6,stroke:#333,stroke-width:2px
class E greenBox
```
1. ### Implementing Task (bump)
1.1 Pallet Scaffolding
> Note: Source code for this article can be found [here](https://github.com/paritytech/polkadot-sdk/pull/5163)
```rust
//! This pallet demonstrates the use of the `pallet::task` for
//! progressing the payment cycle of a ranked collective.
#![cfg_attr(not(feature = "std"), no_std)]
// ...
use frame_system::offchain::CreateInherent; // Undergoing change as per this MR
#[cfg(feature = "experimental")]
use frame_system::offchain::SubmitTransaction;
// Re-export pallet items so that they can be accessed from the crate namespace.
pub use pallet::*;
pub mod mock;
pub mod tests;
#[cfg(feature = "runtime-benchmarks")]
mod benchmarking;
pub mod weights;
pub use weights::*;
#[cfg(feature = "experimental")]
const LOG_TARGET: &str = "pallet-salary-tasks";
// ...
#[frame::pallet]
pub mod pallet {
use super::*;
use frame_support::pallet_prelude::*;
use frame_system::pallet_prelude::*;
// ...
}
```
#### 1.2 Defining Salary Pallet Task (Bump)
```rust
#[pallet::tasks_experimental]
impl<T: Config<I>, I: 'static> Pallet<T, I> {
#[pallet::task_list({
Pallet::<T, I>::salary_task_list()
})]
#[pallet::task_condition(|| {
let now = frame_system::Pallet::<T>::block_number();
let cycle_period = Pallet::<T, I>::cycle_period();
let Some(mut status) = Status::<T, I>::get() else { return false };
status.cycle_start.saturating_accrue(cycle_period);
now >= status.cycle_start
})]
#[pallet::task_weight(T::WeightInfo::bump_offchain())]
#[pallet::task_index(0)]
pub fn bump_offchain() -> DispatchResult {
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
status.cycle_start = frame_system::Pallet::<T>::block_number();
Pallet::<T, I>::do_bump_unchecked(&mut status);
Ok(())
}
}
```
1.2.1 **Task List**: Define Iterable storage list for Candidates
```rust
#[pallet::task_list({
Pallet::<T, I>::salary_task_list()
})]
// ...
fn salary_task_list() -> impl Iterator<Item = ()> {
if let Some(status) = Status::<T, I>::get() {
let now = frame_system::Pallet::<T>::block_number();
let cycle_period = Pallet::<T, I>::cycle_period();
// Check if the current block number is greater than or equal to
// the cycle start + period
if now >= status.cycle_start + cycle_period {
vec![()].into_iter() // Success: one task available, represented by `()`
} else {
vec![].into_iter() // Failure: no task available
}
} else {
vec![].into_iter() // No task available, as there is no status
}
}
```
1.2.2 **Task Condition**: Specify Boolean Check for Task Eligibility
```rust
#[pallet::task_condition(|| {
let now = frame_system::Pallet::<T>::block_number();
let cycle_period = Pallet::<T, I>::cycle_period();
let Some(mut status) = Status::<T, I>::get() else { return false };
status.cycle_start.saturating_accrue(cycle_period);
now >= status.cycle_start
})]
```
1.2.3 **Task Weight**: Assigns computational weight for fee calculation
```rust
#[pallet::task_weight(T::WeightInfo::bump_offchain())]
```
**Define `bump_offchain` benchmark**:
```rust
#[benchmark]
fn bump_offchain() {
let caller = whitelisted_caller();
ensure_member_with_salary::<T, I>(&caller);
Salary::<T, I>::init(RawOrigin::Signed(caller.clone()).into()).unwrap();
Salary::<T, I>::induct(RawOrigin::Signed(caller.clone()).into()).unwrap();
System::<T>::set_block_number(System::<T>::block_number() + Salary::<T, I>::cycle_period());
#[block]
{
Task::<T, I>::bump_offchain().unwrap();
}
assert_eq!(Salary::<T, I>::status().unwrap().cycle_index, 1u32.into());
}
```
1.2.4 **Task Index**: Provides unique identifier for task dispatching
```rust
#[pallet::task_index(0)]
```
1.2.5 **Task definition**: The task that gets transformed into an unsigned transaction
```rust
pub fn bump_offchain() -> DispatchResult {
let mut status = Status::<T, I>::get().ok_or(Error::<T, I>::NotStarted)?;
status.cycle_start = frame_system::Pallet::<T>::block_number();
Pallet::<T, I>::do_bump_unchecked(&mut status);
Ok(())
}
```
Helper function to keep the codebase neat:
```rust
fn do_bump_unchecked(status: &mut StatusOf<T, I>) {
status.cycle_index.saturating_inc();
status.budget = T::Budget::get();
status.total_registrations = Zero::zero();
status.total_unregistered_paid = Zero::zero();
Status::<T, I>::put(status.clone());
Self::deposit_event(Event::<T, I>::CycleStarted {
index: status.cycle_index
});
}
```
1.2.6 Defining off-chain worker that bundles the task
```rust
#[pallet::hooks]
impl<T: Config<I>, I: 'static> Hooks<BlockNumberFor<T>> for Pallet<T, I> {
#[cfg(feature = "experimental")]
fn offchain_worker(_block_number: BlockNumberFor<T>) {
// Create a valid task
let task = Task::<T, I>::BumpOffchain {};
let call = frame_system::Call::<T>::do_task { task: task.into() };
// Submit the task as an unsigned transaction
let xt = <T as CreateInherent<frame_system::Call<T>>>::create_inherent(call.into());
let res = SubmitTransaction::<T, frame_system::Call<T>>::submit_transaction(xt);
match res {
Ok(_) => log::info!(target: LOG_TARGET, "Submitted the task."),
Err(e) => log::error!(target: LOG_TARGET, "Error submitting task: {:?}", e),
}
}
// Allows testing experimental features without breaking production code.
#[cfg(not(feature = "experimental"))]
fn offchain_worker(_block_number: BlockNumberFor<T>) {}
}
```
1.2.7 Adding RuntimeTask configuration
```rust
#[pallet::config]
pub trait Config<I: 'static = ()>:
CreateInherent<frame_system::Call<Self>>
+ frame_system::Config<RuntimeTask: From<Task<Self, I>>>
{...}
```
> `CreateInherent<frame_system::Call<Self>>`
> Executes tasks via unsigned transaction execution.
Bare Extrinsic Creation
```rust
// 1. Transaction Base Configuration
impl<LocalCall> frame_system::offchain::CreateTransactionBase<LocalCall> for Test
where
RuntimeCall: From<LocalCall>,
{
type RuntimeCall = RuntimeCall;
type Extrinsic = Extrinsic;
}
```
Purpose: Establishes fundamental types for transaction handling
RuntimeCall: Enum containing all possible runtime calls
Extrinsic: The actual extrinsic type used in blocks
```rust
// 2. Unsigned transaction Creation Implementation
impl<LocalCall> frame_system::offchain::CreateInherent<LocalCall> for Test
where
RuntimeCall: From<LocalCall>,
{
fn create_inherent(call: Self::RuntimeCall) -> Self::Extrinsic {
Extrinsic::new_bare(call)
}
}
```
Purpose: Converts the call created by the off-chain worker in the salary pallet into an unsigned transaction via:
Bare Extrinsic Creation: new_bare generates an unsigned transaction
Type Conversion: Converts RuntimeCall to chain-specific Extrinsic
```rust
frame_system::Config<RuntimeTask: From<Task<Self, I>>>
```
Enables runtime to understand pallet-specific tasks.
### How this creates Salary Cycle Automation
```rust
// Create a valid task
let task = Task::<T, I>::BumpOffchain {};
let call = frame_system::Call::<T>::do_task { task: task.into() };
// Submit the task as an unsigned transaction
let xt = <T as CreateInherent<frame_system::Call<T>>>::create_inherent(call.into());
```
#### Benchmarking the Task
```bash
# Now run frame-omni-bencher to generate weights for the bump task.
# Compile the runtime implementing pallet-salary, specifically the collectives-westend-runtime.
cargo build -p collectives-westend-runtime --features runtime-benchmarks --release
# Install & run frame-omni-bencher
frame-omni-bencher v1 benchmark pallet \
--runtime ./target/release/wbuild/collective-westend-runtime/collective-westend-runtime.compressed.compact.wasm \
--pallet pallet-salary --extrinsic "" \
--output weights.rs
```
#### When to Use vs Alternative
| Scenario | Solution |
| ---------------------- | ----------- |
| Runtime State-Driven Automation | Task API (This Tutorial) |
| Off-chain State-Driven Automation | [Novasama(Fellowship Observer)](https://github.com/novasamatech/fellowship-observer/pull/1) |
| User-Triggered Actions | Pallet Call |
### Conclusion: Building Autonomous Blockchain Protocols with Substrate's Task API
This tutorial has equipped you to implement state-driven automation in Substrate runtimes, transforming passive chains into self-executing protocols. By leveraging the experimental Task API, you've learned to **safely automate core protocol functions** without manual intervention.
related pull request, reference.
https://github.com/paritytech/polkadot-sdk/pull/7597