# 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