Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Route Actions

Route actions define what happens when a route is invoked. They appear inside the action list after the => arrow. Actions are the imperative side of an entity — they send messages, deploy instances, bind intermediates, and produce return values.

Action List

The action list is enclosed in square brackets. Actions follow one another without separators; only let, var, and emit end with a semicolon:

pay(to: address) => [
    let fee = msg::value / 100;
    let net = msg::value - fee;
    ~> to with { value: net }
    ~> m_treasury with { value: fee }
]

An empty action list is written as []:

increment() => []

Action Types

ActionSyntaxChapter
Send messageroute(args) ~> destSend Messages
Plain transfer~> dest with { value: amount }Send Messages
Deploydeploy Entity(args)Deploy
Emit eventemit Event(args);Emit Events
Conditionalif cond => [actions] else [actions]Conditional Actions
Returnreturn(value)Return Values
Callcall route(args)Call
Let bindinglet x = expr;See below

Let Bindings in Actions

The let keyword introduces a named intermediate value within the action list. Let bindings are terminated by a semicolon:

forward() => [
    let fee = msg::value * 3 / 1000;
    let payout = msg::value - fee;
    ~> m_recipient with { value: payout }
    ~> m_fee_collector with { value: fee }
]

Let bindings are evaluated in order. Later bindings and actions can reference earlier bindings.

Execution Order

An unphased route runs in this order:

  1. from checks, then where checks — any failure reverts the whole route.
  2. Every member transform is computed (from the values the members had before the route) and the results are stored.
  3. Actions execute in declaration order: let bindings are evaluated, sends, deploys, and emits happen, conditionals branch on their guard.

The combination of actions and member transforms defines the complete effect of a route invocation. On EVM this is the checks-effects-interactions order: storage is written before any external call is made.

Actions and State

State mutations happen through member transforms, not through actions. This separation is a core design principle of Cambrian. Transforms are stored before the actions run, but which member value an action reads depends on the action:

  • Arguments of outbound effects — sends (~>, including var captures), call, and deploy — read the value from before the route’s transforms.
  • Everything else — let bindings, emit arguments, if conditions, and return — reads the new value.
entity Observer {
    routes {
        notify(count: u64) => []
    }

    m_last: u64 {
        in notify(count) => count
    }
}

entity Counter {
    event Counted(count: u64);

    routes {
        #[factory_only]
        init setup(observer: Address<Observer>) => []

        increment_and_notify() => [
            // send argument: the value *before* the increment
            notify(m_count) ~> m_observer
            // emit argument: the value *after* the increment
            emit Counted(m_count);
        ]
    }

    m_observer: Address<Observer> {
        in setup(observer) => observer
    }

    m_count: u64 {
        in increment_and_notify() => m_count + 1
    }
}

This lets a route pay out a balance that its own transforms reset to zero, while still writing storage before the transfer:

claim() => [
    ~> msg::sender with { value: m_pending[msg::sender] }
]

with in claim() => m_pending.insert(msg::sender, 0). Write the member read directly in the send: let amount = m_pending[msg::sender]; would already read the reset value (zero). To carry an old value through a let, bind it in an earlier phase.

Complete Example

extern entity Token {
    route transfer(to: address, amount: U256);
}

entity Exchange {
    error SlippageExceeded();

    routes {
        #[factory_only]
        init setup(token: Address<Token>, rate: U256) => []

        buy(min_out: U256)
            where msg::value > 0 : throw 100
        => [
            let amount_out = msg::value * m_rate / 1_000_000;
            if amount_out < min_out => [
                throw SlippageExceeded()
            ] else [
                transfer(msg::sender, amount_out) ~> m_token
            ]
        ]
    }

    m_token: Address<Token> {
        in setup(token, _) => token
    }

    m_rate: U256 {
        in setup(_, rate) => rate
    }
}