Layer LogoWAVS Docs
WAVS builders handbook

Triggers

A trigger prompts a WAVS service to run. Operators listen for the trigger event specified by the service and execute the corresponding component off-chain. Triggers can be any onchain event emitted from any contract.

Trigger lifecycle

  1. A service is deployed with a service.json manifest which contains information on the service and workflow logic (components, triggers, submission logic).

  2. Operators maintain lookup maps to track and verify triggers. For EVM and Cosmos events, they map chain names, contract addresses, and event identifiers to trigger IDs. Block interval triggers are tracked by chain name with countdown timers, while cron triggers are managed in a priority queue ordered by execution time.

  3. When a trigger is detected, operators verify it against their lookup maps according to trigger type. If a match is found, a TriggerAction is created with the trigger configuration and event data.

  4. TriggerAction has 2 fields: TriggerConfig which contains the service, workflow, and trigger configuration, and TriggerDatacontains the trigger data based on the trigger type.

pub struct TriggerAction {
pub config: TriggerConfig, // Contains service_id, workflow_id, and trigger type
pub data: TriggerData, // Contains the actual trigger data
}
pub struct TriggerConfig {
pub service_id: ServiceID,
pub workflow_id: WorkflowID,
pub trigger: Trigger,
}
pub enum TriggerData {
CosmosContractEvent { //For Cosmos event triggers
contract_address: layer_climb_address::Address, /// The address of the contract that emitted the event
chain_name: ChainName, /// The name of the chain where the event was emitted
event: cosmwasm_std::Event, /// The data that was emitted by the contract
block_height: u64, /// The block height where the event was emitted
},
EvmContractEvent { //For EVM event triggers
contract_address: alloy_primitives::Address, /// The address of the contract that emitted the event
chain_name: ChainName, /// The name of the chain where the event was emitted
log: LogData, /// The raw event log
block_height: u64, /// The block height where the event was emitted
},
BlockInterval { //For block interval triggers
chain_name: ChainName, /// The name of the chain where the blocks are checked
block_height: u64, /// The block height where the event was emitted
},
Cron { //For cron triggers
trigger_time: Timestamp, /// The trigger time
},
AtprotoEvent { // For Bluesky/AT Protocol event triggers
sequence: i64, /// Monotonically increasing sequence number
timestamp: i64, /// Event timestamp
repo: String, /// DID of the repository (e.g. "did:plc:...")
collection: String, /// Lexicon collection (e.g. "app.bsky.feed.post")
rkey: String, /// Record key within the collection
action: String, /// Operation type: "create", "update", or "delete"
cid: Option<String>, /// Content identifier of the record (None for deletes)
record_data: Option<String>, /// JSON-encoded record contents (None for deletes)
},
HypercoreAppend { // For Hypercore feed append triggers
feed_key: String, /// Public key identifying the Hypercore feed
index: u64, /// Index of the newly appended block in the feed
data: Vec<u8>, /// Raw bytes of the appended block
},
Raw(Vec<u8>), // Raw bytes — used for local testing with `wasi-exec`
}
  1. The TriggerAction is converted to a WASI-compatible format and passed to the component where it is decoded and processed. The component decodes the incoming event trigger data using the decode_event_log_data! macro from the wavs-wasi-utils crate. Visit the components page for more information on decoding and processing trigger data in your component.

Trigger configuration

Triggers define when and how the component should be executed. Each workflow needs a trigger to be set. They are set in the trigger field of the service.json file.

EVM event trigger

This trigger listens for specific events emitted by contracts on EVM-compatible chains, executing the component when a matching event is detected. Event triggers pass raw log data to the component.

"trigger": {
"evm_contract_event": {
"address": "0x00000000219ab540356cbb839cbe05303d7705fa", // Contract address to monitor
"chain_name": "ethereum", // Chain to monitor
"event_hash": "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef" // Event hash (32 bytes)
}
}

Your evm chain information must be set in wavs.toml under the [default.chains.evm.<chain_name>] section:

wavs.toml
# Mainnet
[default.chains.evm.ethereum]
chain_id = "1"
ws_endpoints = ["wss://eth.drpc.org"] # Supports multiple endpoints for automatic failover
http_endpoint = "https://eth.drpc.org"

You'll need to set your EVM chain credential in your .env file to establish a connection for monitoring the EVM chain:

.env
WAVS_CLI_EVM_CREDENTIAL="0x5ze146f435835b1762ed602088740d201b68fd94bf808f97fd04588f1a63c9ab"

Cosmos event trigger

This trigger monitors events emitted by Cosmos smart contracts, executing your component when a matching event type is detected from the specified contract address. Cosmos event triggers pass the contract data that was emitted by the contract to the component.

"trigger": {
"cosmos_contract_event": {
"address": {
"Cosmos": {
"bech32_addr": "neutron1qlaq54uh9f52d3p66q77s6kh9k9ee3vasy8gkdkk3yvgezcs6zts0mkcv4", // Contract address to monitor
"prefix_len": 7 // Length of the Bech32 prefix (7 for Neutron)
}
},
"chain_name": "neutron", // Chain to monitor
"event_type": "send_nft" // Event type to watch
}
},

Your chain information must be set in wavs.toml under the [default.chains.cosmos.<chain_name>] section:

wavs.toml
# == Cosmos chains ==
[default.chains.cosmos.neutron]
chain_id = "pion-1"
bech32_prefix = "neutron"
rpc_endpoint = "https://rpc-falcron.pion-1.ntrn.tech"
grpc_endpoint = "http://grpc-falcron.pion-1.ntrn.tech:80"
gas_price = 0.0053
gas_denom = "untrn"

Your Cosmos mnemonic must be set in your .env file to establish a connection for monitoring the Cosmos chain:

.env
WAVS_CLI_COSMOS_MNEMONIC="large slab plate twenty laundry illegal vacuum phone drum example topic reason"

Cron trigger

Executes your component on a schedule defined by a cron expression, with optional start and end times to control the execution window. If no start or end time is provided, the component will start immediately and run indefinitely. Cron triggers pass the trigger timestamp to the component.

"trigger": {
"cron": {
"schedule": "0 */5 * * * *", // Every 5 minutes (at 0 seconds)
"start_time": 1704067200000000000, // Optional start time in nanoseconds since Unix epoch (2024-01-01T00:00:00Z) (default: null)
"end_time": 1735689599000000000 // Optional end time in nanoseconds since Unix epoch (default: null)
}
}
// Cron Expression Format:
// * * * * * *
// │ │ │ │ │ │
// │ │ │ │ │ └── Day of week (0-6, where 0 is Sunday)
// │ │ │ │ └──── Month (1-12)
// │ │ │ └────── Day of month (1-31)
// │ │ └──────── Hour (0-23)
// │ └────────── Minute (0-59)
// └──────────── Second (0-59)
//
// Each field can be:
// - A number: `5` (exact time)
// - A range: `1-5` (1 through 5)
// - A list: `1,3,5` (1, 3, and 5)
// - A step: `*/5` (every 5 units)
// - An asterisk: `*` (every unit)
//
// Common examples:
// - `0 */5 * * * *` - Every 5 minutes (at 0 seconds)
// - `0 0 */6 * * *` - Every 6 hours (at 0 minutes and 0 seconds)
// - `0 0 0 * * *` - Every day at midnight (00:00:00)
// - `0 0 12 * * *` - Every day at noon (12:00:00)
// - `0 0 12 1 * *` - Noon on the first day of every month
// - `0 0 12 * * 1` - Noon every Monday

Crontab.guru is a helpful tool for making cron expressions.

Cron trigger latency

There may be slight variations in Cron trigger execution time between operators due to network latency and clock drift. Cron triggers are best suited for tasks that don't require precise synchronization between operators:

  • Triggering components that don't need exact synchronization.
  • Collecting data from external services with monotonic pagination.
  • Background tasks where eventual consistency is acceptable.

If you need precise timing synchronization between operators, consider using block-based triggers instead.

Block trigger

Executes your component at regular block intervals on a specified EVM or Cosmos chain, useful for chain-specific operations that need to run periodically. Block interval triggers pass the block height and chain name to the component.

"trigger": {
"block_interval": {
"chain_name": "ethereum-mainnet",
"n_blocks": 10,
"start_block": null, // Optional blockheight to start
"end_block": null // Optional blockheight to end
}
}

AT Protocol (Bluesky) event trigger

Executes your component when a record is created, updated, or deleted on the AT Protocol network (the protocol underlying Bluesky). Useful for building services that react to social activity, content moderation, or decentralized social graph changes.

"trigger": {
"atproto_event": {
"collection": "app.bsky.feed.post", // Lexicon collection to watch
"action": "create" // "create", "update", or "delete"
}
}

The TriggerData::AtprotoEvent variant passed to your component contains:

TriggerData::AtprotoEvent(TriggerDataAtprotoEvent {
sequence, // Monotonically increasing sequence number
timestamp, // Event timestamp
repo, // DID of the repository (e.g. "did:plc:...")
collection, // Lexicon collection (e.g. "app.bsky.feed.post")
rkey, // Record key within the collection
action, // "create", "update", or "delete"
cid, // Content identifier (None for deletes)
record_data, // JSON-encoded record contents (None for deletes)
..
})

Hypercore append trigger

Executes your component when a new block is appended to a Hypercore feed. Useful for building services that process peer-to-peer data streams.

"trigger": {
"hypercore_append": {
"feed_key": "abc123..." // Public key of the Hypercore feed to watch
}
}

The TriggerData::HypercoreAppend variant contains:

TriggerData::HypercoreAppend(TriggerDataHypercoreAppend {
feed_key, // Public key identifying the Hypercore feed
index, // Index of the newly appended block
data, // Raw bytes of the appended block
})

Raw trigger (local testing)

The Raw variant is not a service trigger type configured in service.json. It is the trigger data format used when executing a component locally with task wasi:exec in the template. Raw bytes are passed directly to the component without any ABI decoding.

// In trigger.rs — handle the Raw variant for local testing
TriggerData::Raw(data) => Ok((0, data.clone(), Destination::CliOutput)),

See the component overview for a full example of handling both on-chain and raw trigger data in the same component.

Edit on GitHub

On this page