Skip to main content

Introduction

Cowboy’s Actor Message Scheduler is a protocol-level mechanism that enables truly autonomous actors through native timer support. Unlike traditional blockchains that require external infrastructure for scheduled execution, Cowboy embeds timers directly into the consensus layer.
Key Innovation: Actors can schedule their own future execution without relying on centralized keepers, cron jobs, or external bots.
Examples in this page are conceptual and illustrative (non-normative). Final interfaces are defined by the SDK and normative CIPs; do not treat these snippets as fixed APIs.

The Problem with Traditional Chains

No Native Time Awareness

On many traditional chains, contracts cannot self-schedule execution; external actors must trigger functions, which introduces centralization and reliability risks.
// ❌ Ethereum: This never executes itself
contract Liquidator {
    function checkPosition() public {
        if (position.isUndercollateralized()) {
            liquidate();
        }
        // Who calls this? When? 🤷
    }
}
Current workarounds all have significant drawbacks:
Approach: Off-chain server runs scheduled tasksProblems:
  • ❌ Centralized (single point of failure)
  • ❌ Requires infrastructure maintenance
  • ❌ Trust assumption (will it run?)
  • ❌ No SLA guarantees

Cowboy’s Solution: Native Protocol Timers

How It Works

Actors can schedule future execution by specifying a target height and a Gas Bidding Agent (GBA). The protocol will trigger the handler when due, under a fixed per‑block timer budget (CIP‑1). The GBA dynamically prices execution based on protocol‑supplied context.

Core Components

Hierarchical Calendar Queue

Three-tier queue system for O(1) scheduling performance

Gas Bidding Agents

Dynamic pricing for timer execution based on urgency

Per-Block Budget

Fixed resource allocation prevents DoS attacks

Priority Queue

Highest-bidding timers execute first during congestion

Hierarchical Calendar Queue

Three-Tier Architecture

Timers are organized into three layers based on their execution distance:
+--------------------------------------------------------------------+
|  Layer 1: Block Ring Buffer                                        |
|  For: Near-term buckets                                            |
|  Performance: O(1) enqueue/dequeue                                 |
+-------------------------------+------------------------------------+
                                | Promote as epoch approaches
                                v
+--------------------------------------------------------------------+
|  Layer 2: Epoch Queue                                              |
|  For: Mid-term horizons                                            |
|  Performance: Amortized maintenance                                |
+-------------------------------+------------------------------------+
                                | Promote as timers enter range
                                v
+--------------------------------------------------------------------+
|  Layer 3: Overflow Sorted Set                                      |
|  For: Long-term timers                                             |
|  Performance: O(log N) ops                                         |
+--------------------------------------------------------------------+

Layer 1: Block Ring Buffer

Purpose: Handle timers scheduled for the immediate future. Structure:
Layer 1 (ring buffer) maps imminent blocks to buckets; placing and retrieving timers is O(1). Exact sizes are governance‑defined per CIP1.
Operations:
Enqueue/dequeue at Layer 1 are O(1); due timers for the current height are retrieved from the corresponding bucket.
Example:
Current block: 1000
Timer scheduled for block 1050

Bucket: 1050 % 256 = 26
Action: Add to ring_buffer[26]

At block 1050:
Action: Retrieve all timers from ring_buffer[26]

Layer 2: Epoch Queue

Purpose: Hold timers scheduled for the next few hours/days. Structure:
Layer 2 (epoch queue) groups timers by future epochs; sizes and epochs are governance‑defined. At epoch boundaries, timers entering range are promoted into Layer 1.
Epoch Maintenance:
Epoch maintenance promotes timers from the current epoch bucket into ring buckets; all work is amortized and bounded (CIP1).
Example:
Current block: 1000 (epoch 2)
Timer scheduled for block 1500 (epoch 4)

Action: Add to epoch_queue[4]

At block 1440 (start of epoch 4):
Action: Move all timers from epoch_queue[4] to ring_buffer

Layer 3: Overflow Sorted Set

Purpose: Store very long-term timers. Structure:
Layer 3 (overflow sorted set) holds very long‑horizon timers in a Merkleized balanced tree, providing O(log N) operations and efficient range queries.
Maintenance:
Overflow migration moves timers that enter the migration horizon into their epoch buckets during epoch maintenance.
Example:
Current block: 1000
Timer scheduled for block 50,000

Action: Insert into overflow_set

Later, when block 45,000 arrives:
Action: Migrate from overflow_set to epoch_queue

Performance Characteristics

OperationLayer 1Layer 2Layer 3
EnqueueO(1)O(1)O(log N)
DequeueO(1)Amortized O(1)O(log N)
MemoryFixedO(epochs)O(distant timers)
RangeNear-termMid-termLong-term
Result: Efficiently handles millions of timers with O(1) performance for the common case.

Dynamic Gas Bidding

The Challenge

Fixed pre‑payment for future execution is inflexible under dynamic network conditions; actors need dynamic bidding at execution time.

The Solution: Gas Bidding Agents (GBA)

Concept: Actors specify a smart contract (GBA) that dynamically determines the execution bid at runtime.
Gas Bidding Agents (GBA) are read‑only contracts queried at execution time; they return bidding parameters (e.g., max_fee_per_cycle, tip_per_cycle) using protocol‑supplied context (trigger/current heights, basefees, last block usage, owner balance) per CIP1.

GBA Context

The protocol provides rich context to GBAs for informed bidding:
class BiddingContext:
    # Timing information
    trigger_block_height: int      # When timer was scheduled
    current_block_height: int      # Current height
    
    # Fee market information
    basefee_cycle: int             # Current cycle basefee
    basefee_cell: int              # Current cell basefee
    last_block_cycle_usage: int    # Congestion indicator
    
    # Actor information
    owner_actor_balance: int       # Available funds for payment
Example Use Cases:
def get_gas_bid(self, context):
    # Check collateral ratio
    position = load_position(self.position_id)
    risk = calculate_liquidation_risk(position)
    
    # Bid based on risk
    if risk > 0.9:  # Very high risk
        return ultra_aggressive_bid()
    elif risk > 0.7:
        return aggressive_bid()
    else:
        return normal_bid()

DoS Protection & Congestion Handling

Fixed Per-Block Budget

Mechanism: Hard limit on timer resource consumption per block.
At each block: collect due timers, query GBA for bids, order by effective tip, execute until the per‑block timer budget is exhausted, roll over the rest (CIP1).
Benefits:
  • ✅ Regular user transactions always have guaranteed space
  • ✅ Timers cannot monopolize block resources
  • ✅ Predictable block processing time
  • ✅ DoS-resistant

Priority Queue Execution

Algorithm:
Execution order is by effective tip (min(tip_per_cycle, max_fee_per_cycle − basefee_cycle)); deterministic ordering and rollover rules ensure bounded work and liveness (CIP1/CIP3).
Example Scenario:
Block 1000 timer queue (10 timers, budget for 6):

Priority Queue (sorted by tip):
1. Liquidation Bot A: tip=1000 → Execute ✅
2. Liquidation Bot B: tip=800  → Execute ✅
3. Oracle Update: tip=700      → Execute ✅
4. DeFi Rebalance: tip=500     → Execute ✅
5. NFT Auction End: tip=400    → Execute ✅
6. Game Tick: tip=300          → Execute ✅
--- Budget exhausted (10M cycles) ---
7. Low Priority Bot: tip=200   → Rollover to block 1001
8. Reminder Service: tip=100   → Rollover to block 1001
9. Data Archive: tip=50        → Rollover to block 1001
10. Test Timer: tip=10         → Rollover to block 1001

Best-Effort Delivery

Rollover Mechanism:
def rollover_to_next_block(timer):
    """
    Unexecuted timers automatically move to next block.
    """
    # Increment delay counter
    timer.delay_count += 1
    
    # Add to next block's queue
    next_block = current_block + 1
    bucket = next_block % RING_BUFFER_SIZE
    ring_buffer[bucket].append(timer)
    
    # Optional: Penalties for excessive delays
    if timer.delay_count > MAX_ALLOWED_DELAYS:
        # Force execute or cancel with penalty
        handle_excessive_delay(timer)
Properties:
  • Liveness: Timers eventually execute (assuming sufficient balance)
  • No data loss: Automatic rollover
  • Fair competition: Higher bids still prioritized on retry
  • Economic feedback: Repeated delays signal need for higher bids

Use Cases

DeFi Liquidation Bot

DeFi liquidation bots can use timers to regularly check positions and act promptly; GBA logic can bid more under volatility.

Oracle Price Updates

Oracles can schedule periodic updates and combine with off‑chain tasks for data fetching.

Game Loop

Games can use timers for deterministic ticks within blockchain cadence.

Subscription Service

Subscription services can schedule periodic renewal checks, relying on the scheduler’s budget and bidding for priority when needed.

Next Steps

Further Reading