EVM Log Trigger

The EVM Log trigger fires when a specific log (event) is emitted by a smart contract on an EVM-compatible blockchain. This capability allows you to build powerful, event-driven workflows that react to onchain activity.

This guide explains the two key parts of working with log triggers:

Configuring your trigger

There are two methods for configuring a log trigger. Choose the one that best fits your use case:

  • Method 1: Using Binding Helpers: Use this approach if you are listening for a single event from a single contract. This is the recommended and simplest approach for most scenarios.
  • Method 2: Manual Configuration: Use this approach when you need to listen for multiple events at once, or for the same event from multiple different contracts.

This guide covers both methods in detail below.

For the most common use case—listening to a single event from a single contract—the recommended approach is to use the helper functions generated by cre generate-bindings evm. The generator creates a LogTrigger<EventName>Log method for each event in your contract's ABI. This method is simple, readable, and type-safe.

The following example assumes you have generated bindings for a contract that emits a standard ERC20 Transfer(address,address,uint256) event.

import (
    "fmt"
    "log/slog"

    "github.com/ethereum/go-ethereum/common"
    "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
    "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm/bindings"
    "github.com/smartcontractkit/cre-sdk-go/cre"
    "your-module-name/contracts/evm/src/generated/my_token" // Replace with your module name from go.mod
)

// When using binding helpers, your handler receives a *bindings.DecodedLog with the decoded event data
func onEvmTrigger(config *Config, runtime cre.Runtime, payload *bindings.DecodedLog[my_token.TransferDecoded]) (*MyResult, error) {
    logger := runtime.Logger()

    // Access the decoded event fields directly from payload.Data
    from := payload.Data.From
    to := payload.Data.To
    value := payload.Data.Value

    logger.Info("Transfer detected",
        "from", from.Hex(),
        "to", to.Hex(),
        "value", value.String(),
        "blockNumber", payload.Log.BlockNumber.String(),
    )

    return &MyResult{}, nil
}

func InitWorkflow(config *Config, logger *slog.Logger, secretsProvider cre.SecretsProvider) (cre.Workflow[*Config], error) {
    // Create an EVM client for the chain you want to monitor
    chainSelector, err := evm.ChainSelectorFromName(config.ChainName)
    if err != nil {
        return nil, fmt.Errorf("failed to get chain selector: %w", err)
    }

    evmClient := &evm.Client{
        ChainSelector: chainSelector,
    }

    // Initialize your contract binding (note: returns 2 values)
    contractAddress := common.HexToAddress(config.TokenAddress)
    myTokenContract, err := my_token.NewMyToken(evmClient, contractAddress, nil)
    if err != nil {
        return nil, fmt.Errorf("failed to create contract binding: %w", err)
    }

    // Use the generated helper to create the trigger for the Transfer event
    logTrigger, err := myTokenContract.LogTriggerTransferLog(
        chainSelector, // The chain to monitor (as uint64)
        evm.ConfidenceLevel_CONFIDENCE_LEVEL_FINALIZED, // The confidence level
        []my_token.Transfer{}, // Empty slice = listen to all Transfer events
    )
    if err != nil {
        return nil, fmt.Errorf("failed to create log trigger: %w", err)
    }

    // Register the handler that will be called when the event is detected
    return cre.Workflow[*Config]{
        cre.Handler(logTrigger, onEvmTrigger),
    }, nil
}

Understanding the DecodedLog payload

The *bindings.DecodedLog[T] payload has two main parts:

  • payload.Data: The decoded event struct with your event fields (e.g., From, To, Value)
  • payload.Log: The raw log metadata (block number, transaction hash, log index, etc.)

Example: Accessing decoded event data

func onEvmTrigger(config *Config, runtime cre.Runtime, payload *bindings.DecodedLog[my_token.TransferDecoded]) (*MyResult, error) {
    logger := runtime.Logger()

    // Access decoded event fields from payload.Data
    from := payload.Data.From        // address (common.Address)
    to := payload.Data.To            // address (common.Address)
    value := payload.Data.Value      // uint256 (*big.Int)

    // Access raw log metadata from payload.Log
    blockNumber := payload.Log.BlockNumber  // *pb.BigInt
    txHash := payload.Log.TxHash            // []byte (32-byte transaction hash)
    logIndex := payload.Log.Index           // uint32

    logger.Info("Transfer detected",
        "from", from.Hex(),
        "to", to.Hex(),
        "value", value.String(),
        "blockNumber", blockNumber.String(),
        "txHash", common.BytesToHash(txHash).Hex(),
    )

    return &MyResult{}, nil
}

Filtering by indexed parameters with the helper

The third parameter of the LogTrigger<EventName>Log function (filters) allows you to create filters based on the values of indexed event parameters.

  • indexed Parameters Only: You can only filter on parameters marked as indexed in the Solidity event definition. Filtering on non-indexed parameters must be done inside your workflow after the event is decoded.
  • Filter Logic: The helper supports both AND and OR conditions:
    • It creates an AND condition between different indexed parameters (e.g., From AND To).
    • It creates an OR condition for multiple values provided for the same indexed parameter.

Example 1: "AND" Filtering (from Alice "AND" to Bob)

To trigger only on transfers from a specific sender to a specific receiver, provide both values in the same struct literal.

// This trigger will only fire for transfers FROM Alice AND TO Bob.
logTrigger, err := myTokenContract.LogTriggerTransferLog(
    config.ChainSelector,
    evm.ConfidenceLevel_CONFIDENCE_LEVEL_FINALIZED,
    []my_token.Transfer{
        { From: common.HexToAddress("0xAlice"), To: common.HexToAddress("0xBob") },
    },
)

Example 2: "OR" Filtering (from Alice "OR" from Charlie)

To trigger on a transfer from one of several possible senders, provide multiple struct literals, each with a value for the same field.

// This trigger will fire for any transfer sent by Alice OR Charlie.
logTrigger, err := myTokenContract.LogTriggerTransferLog(
    config.ChainSelector,
    evm.ConfidenceLevel_CONFIDENCE_LEVEL_FINALIZED,
    []my_token.Transfer{
        { From: common.HexToAddress("0xAlice") },
        { From: common.HexToAddress("0xCharlie") },
    },
)

Example 3: "AND/OR" Filtering

You can combine AND and OR conditions for even more precise filtering. The following example triggers if a Transfer is (from Alice AND to Bob) OR (from Charlie AND to David).

logTrigger, err := myTokenContract.LogTriggerTransferLog(
    config.ChainSelector,
    evm.ConfidenceLevel_CONFIDENCE_LEVEL_FINALIZED,
    []my_token.Transfer{
        { From: common.HexToAddress("0xAlice"), To: common.HexToAddress("0xBob") },
        { From: common.HexToAddress("0xCharlie"), To: common.HexToAddress("0xDavid") },
    },
)

For more complex, grouped AND/OR conditions (e.g., (from Alice AND to Bob) OR (from Charlie AND to David), you must use the manual configuration method below.

Confidence Level

The second parameter of the LogTrigger<EventName>Log function specifies the block confirmation level to wait for before triggering. For more details on the available levels, see the evm.FilterLogTriggerRequest reference.

Method 2: Manual configuration (advanced)

For more complex scenarios, you can manually construct the evm.FilterLogTriggerRequest. This method is necessary when you need to listen to multiple events or the same event from multiple contracts with a single trigger.

Feature
Using Binding HelpersManual Configuration
Primary use caseFiltering on a single event type from a single contractFiltering across multiple event types or the same event from multiple contracts
Simplicity & readability✅ High❌ Low
Type safety✅ High❌ Low (manual hash management)

The main advantage of manual configuration is its filtering capability. This is achieved by directly manipulating the fields of the evm.FilterLogTriggerRequest struct.

Here is a simplified view of its structure:

logTriggerCfg := &evm.FilterLogTriggerRequest{
    Addresses: [][]byte{ ... },
    Topics:    []*evm.TopicValues{ ... },
}

To manually configure a trigger, you specify the contract addresses and the event topics to filter by. The Topics array is where you define both the event type you want to listen for, and any conditions on its indexed parameters.

Understanding topic filtering

An EVM log filter uses these fields to create precise rules:

  • The Addresses List: The trigger will fire if the event is emitted from any contract in this list (OR logic).
  • The Topics Array: An event must match the conditions for all defined topic slots (AND logic between topics). Within a single topic, you can provide a list of values, and it will match if the event's topic is any of those values (OR logic within a topic).

This AND/OR logic is what enables advanced patterns. The first and most important topic, Topics[0], is used to filter by event type. Its value should be the Keccak-256 hash of the event's signature. For example, the signature for a standard ERC20 transfer is "Transfer(address,address,uint256)". You provide the hash of this string as Topics[0] to filter for only Transfer events. The subsequent topics, Topics[1], Topics[2], and Topics[3], are then used to filter on that event's indexed parameters.

Filtering by indexed parameters

Example 1: "AND" filtering

To trigger only on a Transfer from Alice AND to Bob, you must manually set Topics[1] (for the first indexed parameter, from) and Topics[2] (for the second, to).

logTriggerCfg := &evm.FilterLogTriggerRequest{
    Addresses: [][]byte{ common.HexToAddress("0xYourTokenContract").Bytes() },
    Topics: []*evm.TopicValues{
        { Values: [][]byte{ myTokenContract.Codec.TransferLogHash() } },       // Topics[0]: Event signature must be Transfer
        { Values: [][]byte{ common.HexToAddress("0xAlice").Bytes() } }, // Topics[1]: `from` must be Alice
        { Values: [][]byte{ common.HexToAddress("0xBob").Bytes() } },  // Topics[2]: `to` must be Bob
    },
}

Examples 2: "OR" filtering

You can use the OR logic within a topic or across the Addresses list to monitor a broader set of onchain activities.

Example 2.A: Listening to multiple event types

To trigger on either a Transfer OR an Approval from a single contract, you provide multiple values for Topics[0]:

logTriggerCfg := &evm.FilterLogTriggerRequest{
    Addresses: [][]byte{ common.HexToAddress("0xYourContractAddress").Bytes() },
    Topics: []*evm.TopicValues{
        { // Topic 0: The event signature
            Values: [][]byte{
                myTokenContract.Codec.TransferLogHash(), // either a Transfer...
                myTokenContract.Codec.ApprovalLogHash(), // ...OR an Approval.
            },
        },
    },
}

Example 2.B: Listening to the same event from multiple contracts

To trigger on a Transfer event from TokenA OR TokenB OR TokenC, you provide multiple addresses in the Addresses list:

logTriggerCfg := &evm.FilterLogTriggerRequest{
    // Listen for events from ANY of these addresses
    Addresses: [][]byte{
        common.HexToAddress("0xTokenContract_A").Bytes(),
        common.HexToAddress("0xTokenContract_B").Bytes(),
        common.HexToAddress("0xTokenContract_C").Bytes(),
    },
    Topics: []*evm.TopicValues{
        { // Topic for the Transfer event signature
            Values: [][]byte{ myTokenContract.Codec.TransferLogHash() },
        },
    },
}

Example 3: "AND/OR" filtering

You can combine all of these techniques to create highly specific filters. For example, to trigger a workflow if a Transfer event is emitted by:

  • either TokenA OR TokenB,
  • AND that transfer is TO your Vault,
  • AND it is FROM either Alice OR Charlie:
logTriggerCfg := &evm.FilterLogTriggerRequest{
    // OR condition on addresses
    Addresses: [][]byte{
        common.HexToAddress("0xTokenContract_A").Bytes(),
        common.HexToAddress("0xTokenContract_B").Bytes(),
    },
    Topics: []*evm.TopicValues{
        // AND condition for Topic 0 (must be a Transfer)
        { Values: [][]byte{ myTokenContract.Codec.TransferLogHash() } },
        // AND condition for Topic 1 (`from` must be...)
        { Values: [][]byte{
            common.HexToAddress("0xAlice").Bytes(),   // ...Alice OR
            common.HexToAddress("0xCharlie").Bytes(), // ...Charlie
        }},
        // AND condition for Topic 2 (`to` must be your vault)
        { Values: [][]byte{ common.HexToAddress("0xYourVaultAddress").Bytes() } },
    },
}

Confidence Level

You can set the block confirmation level by adding the Confidence field to the evm.FilterLogTriggerRequest struct. See the reference for more details on the available levels.

Decoding the event payload

What your handler receives depends on how you configured your trigger:

  • If you used binding helpers: Your handler automatically receives *bindings.DecodedLog[<EventName>Decoded] with the event data already decoded. You don't need to do anything else - just access payload.Data.<FieldName>.

  • If you used manual configuration: Your handler receives a raw *evm.Log struct that you must decode yourself. The sections below show you how.

For the full type definition of *evm.Log and all available fields, see the EVM Log Trigger SDK Reference.

Method 1: Using the binding codec (for manual configuration)

If you used manual configuration but have bindings for the contract that emitted the event, you should use the generated Codec to decode the *evm.Log payload your handler receives. The Codec provides a safe, simple, and type-safe way to get your data.

import (
    "your-module-name/contracts/evm/src/generated/my_token"
    "github.com/ethereum/go-ethereum/common"
    "github.com/smartcontractkit/cre-sdk-go/capabilities/blockchain/evm"
    "github.com/smartcontractkit/cre-sdk-go/cre"
)

// The `onEvmTrigger` handler receives the raw event log.
func onEvmTrigger(config *Config, runtime cre.Runtime, log *evm.Log) (*MyResult, error) {
    logger := runtime.Logger()
    // Assume `myTokenContract` is an initialized instance of your contract binding.

    // Check which event was received by comparing the first topic to known event topics.
    eventSignature := common.BytesToHash(log.Topics[0])

    switch eventSignature {
    case common.BytesToHash(myTokenContract.Codec.TransferLogHash()):
        // It's a Transfer! Use the safe, generated decoder.
        transferEvent, err := myTokenContract.Codec.DecodeTransfer(log)
        if err != nil { /* handle error */ }
        logger.Info("Transfer detected", "from", transferEvent.From, "to", transferEvent.To)

    case common.BytesToHash(myTokenContract.Codec.ApprovalLogHash()):
        // It's an Approval! Use the safe, generated decoder.
        approvalEvent, err := myTokenContract.Codec.DecodeApproval(log)
        if err != nil { /* handle error */ }
        logger.Info("Approval detected", "owner", approvalEvent.Owner, "spender", approvalEvent.Spender)
    }

    return &MyResult{}, nil
}

Method 2: Manual decoding (for manual configuration without bindings)

If you used manual configuration and are interacting with a third-party contract for which you do not have bindings, you must manually parse the raw byte arrays in log.Topics (for indexed fields) and log.Data (for non-indexed fields).

The following example shows how to manually parse a standard ERC20 Transfer(address indexed from, address indexed to, uint256 value) event.

import "github.com/ethereum/go-ethereum/accounts/abi"
import "github.com/ethereum/go-ethereum/common"
import "math/big"
import "fmt"

type MyResult struct{}

func onEvmTrigger(config *Config, runtime cre.Runtime, log *evm.Log) (*MyResult, error) {
    logger := runtime.Logger()
    // Manually parse the indexed topics. `Topics[0]` is the event signature.
    // An indexed `address` is a 32-byte value; we slice the last 20 bytes to get the actual address.
    fromAddress := common.BytesToAddress(log.Topics[1][12:])
    toAddress := common.BytesToAddress(log.Topics[2][12:])

    // Manually parse the non-indexed data using the go-ethereum ABI package.
    var value *big.Int
    uint256Type, _ := abi.NewType("uint256", "", nil)
    decodedData, err := abi.Arguments{{Type: uint256Type}}.Unpack(log.Data)
    if err != nil {
        return nil, fmt.Errorf("failed to unpack log data: %w", err)
    }
    value = decodedData[0].(*big.Int)

    logger.Info(
        "Manual transfer decode successful",
        "from", fromAddress.Hex(),
        "to", toAddress.Hex(),
        "value", value.String(),
    )

    // ... Your logic here ...
    return &MyResult{}, nil
}

Testing log triggers in simulation

To test your EVM log trigger during development, you can use the workflow simulator with a transaction hash and event index. The simulator fetches the log from your configured RPC and passes it to your callback function.

For detailed instructions on simulating EVM log triggers, including interactive and non-interactive modes, see the EVM Log Trigger section in the Simulating Workflows guide.

Get the latest Chainlink content straight to your inbox.