Skip to content

SDK Macros

Everything an app exposes to the runtime is declared with macros re-exported under calimero_sdk::app. They are defined in crates/sdk/macros/src/lib.rs and brought into scope with use calimero_sdk::app;.

MacroKindApplies toWhat it does
#[app::state]attributestructDeclares the persistent application state.
#[app::logic]attributeimpl blockTurns methods into runtime entry points.
#[app::init]attributemethodMarks the constructor run when the app is first installed in a context.
#[app::view]attribute&self methodMarks a read-only method (shared lock; no mutation).
#[app::xcall]attributemethodMarks a method callable from other contexts via cross-context call.
#[app::destroy]attributemethodOpts a method into state destruction (consuming self).
#[app::event]attributestruct / enumDeclares emittable events.
#[app::private]attributestruct / enumDeclares node-local (un-synchronized) state.
#[app::migrate]attributefunctionExports a state-migration function for upgrades.
#[app::migration_check]attributefn(old, new) -> boolExports a predicate the runtime runs to accept/abort a migration.
#[derive(app::Mergeable)]derivestructDerives Mergeable so a custom struct can be a CRDT value.
#[derive(app::Migrate)]derivestructGenerates a migration from per-field rules.
app::emit!(..)function-likeexpressionEmits an event, optionally with a handler.
app::log!(..)function-likeexpressionLogs a formatted message to the runtime.
app::err!(..)function-likeexpressionBuilds an Err(app::Error::..).
app::bail!(..)function-likeexpressionReturns early with an error.

Before the per-macro reference, here is a complete, runnable skeleton that uses the four macros every app needs: state, an init constructor, a mutating method, and a read-only view. Each numbered comment maps to a macro described below.

use calimero_sdk::app;
use calimero_storage::collections::{LwwRegister, UnorderedMap};
// 1. The single persistent state struct, built from CRDT collections.
#[app::state(emits = for<'a> Event<'a>)]
pub struct KvStore {
items: UnorderedMap<String, LwwRegister<String>>,
}
// 2. The events this app may emit (named in `emits = ..` above).
#[app::event]
pub enum Event<'a> {
Inserted { key: &'a str, value: &'a str },
}
#[app::logic]
impl KvStore {
// 3. Constructor: runs once, when the app is first installed in a context.
#[app::init]
pub fn init() -> KvStore {
KvStore { items: UnorderedMap::new() }
}
// 4. Mutating method: takes `&mut self`, so it commits state on success.
pub fn set(&mut self, key: String, value: String) -> app::Result<()> {
app::emit!(Event::Inserted { key: &key, value: &value });
self.items.insert(key, value.into())?;
Ok(())
}
// 5. Read-only view: takes `&self`, runs under a shared lock, never commits.
#[app::view]
pub fn get(&self, key: String) -> app::Result<Option<String>> {
Ok(self.items.get(&key)?.map(|v| v.get().clone()))
}
}

Declares the single persistent state struct. It must be a struct — applying it to an enum is a compile error (no Mergeable impl can be generated for an enum). State is built from CRDT collections so concurrent edits merge deterministically. The macro accepts two optional parameters:

  • emits = <EventType> — the event type this app may emit (use a for<'a> binder for events that borrow, as below).
  • version = N — a schema version, forwarded to #[derive(app::Migrate)] for migrations.
use calimero_sdk::app;
use calimero_storage::collections::{LwwRegister, UnorderedMap};
#[app::state(emits = for<'a> Event<'a>)]
pub struct KvStore {
items: UnorderedMap<String, LwwRegister<String>>,
}

Applied to an impl block, this exposes its public methods as runtime entry points: arguments are decoded from the caller’s JSON, the receiver is loaded from state, and the return value is encoded back. Methods return app::Result<T>.

#[app::logic]
impl KvStore {
pub fn set(&mut self, key: String, value: String) -> app::Result<()> {
self.items.insert(key, value.into())?;
Ok(())
}
}

Marks the constructor that produces the initial state when an app is first installed into a context. Takes no self; returns the state.

#[app::logic]
impl KvStore {
#[app::init]
pub fn init() -> KvStore {
KvStore { items: UnorderedMap::new() }
}
}

Marks a read-only method. It is recorded in the ABI as read-only so the node can take a shared read lock instead of an exclusive write lock. It must take &self (a &mut self receiver is a compile error) and is mutually exclusive with #[app::init] and #[app::xcall].

#[app::view]
pub fn get(&self, key: String) -> app::Result<Option<String>> {
Ok(self.items.get(&key)?.map(|v| v.get().clone()))
}

Marks a method as permitted to consume self for state destruction. Without it, a method taking self by value is rejected — the macro nudges you to &self / &mut self so you don’t tear down state by accident.

Marks a public method as a cross-context entry point. Only methods bearing this attribute can be reached by another context in the same namespace via env::xcall. It is mutually exclusive with #[app::init] and #[app::view] (an xcall is fire-and-forget, so a view’s return value would have nowhere to go).

use calimero_sdk::{app, env};
#[app::logic]
impl PingPong {
// caller side: queue a call, executed after this method returns
pub fn ping(&self, target_context: [u8; 32]) -> app::Result<()> {
let params = calimero_sdk::serde_json::to_vec(&"ping").unwrap();
env::xcall(&target_context, "pong", &params);
Ok(())
}
// callee side: only reachable because it is an xcall entry point
#[app::xcall]
pub fn pong(&mut self) -> app::Result<()> {
// `xcall_origin()` is set by the node and cannot be forged;
// `None` means a direct (non-xcall) call — reject it if you require xcall.
let from = env::xcall_origin();
let _ = from;
Ok(())
}
}

See apps/xcall-example for the full ping-pong with provenance checks.

#[app::event] declares an enum (or struct) of events your app can emit; app::emit! emits one. The single-argument form emits the event; the two-element-tuple form names a handler method to run after the event is processed.

#[app::event]
pub enum Event<'a> {
Inserted { key: &'a str, value: &'a str },
Removed { key: &'a str },
}
// inside a logic method:
app::emit!(Event::Inserted { key: &key, value: &value });
// with a handler method name:
app::emit!((Event::Inserted { key: &key, value: &value }, "on_insert"));

Declares a struct whose data is stored node-local and is not synchronized across the network — for secrets, node-specific config, or caches. Tree-backed collection fields are stored through the private storage backend rather than the synchronized CRDT store. See apps/private_data.

#[app::private]
pub struct Secrets {
api_token: Option<String>,
}

When you ship a new schema, the runtime needs a way to carry the old state forward. Two macros cover this (see apps/migration-harness-example).

Exports a migration function. The hand-written form reads the old root with calimero_sdk::read_raw(), transforms it, and returns the new state. The macro generates the #[no_mangle] extern "C" export, sets up the panic hook, and borsh-serializes the result.

#[app::migrate]
pub fn migrate() -> AppV2 {
let old: AppV1 = calimero_sdk::read_raw().expect("old root");
AppV2 { /* carry/transform fields from `old` */ }
}

Generates the #[app::migrate] body from per-field rules, so you write only the diff. Field-level #[migrate(...)] keys: new = EXPR (seed an added field), from = old_field (rename), with = EXPR (transform, e.g. a type change); the struct-level from, method, and emit configure the source layout, the generated function name, and an event to emit.

#[app::state(version = 2, emits = for<'a> MigrateEvent<'a>)]
#[derive(app::Migrate)]
#[migrate(from = AppV1, method = migrate, emit = MigrateEvent::Migrated)]
pub struct AppV2 {
items: UnorderedMap<String, LwwRegister<String>>, // carried by name
#[migrate(new = LwwRegister::new("note".to_owned()))]
notes: LwwRegister<String>, // additive
#[migrate(from = legacy)]
renamed: LwwRegister<String>, // renamed source
}

Exports a predicate the runtime runs on the produced new root before it is committed. Returning false (or panicking) logically aborts the migration, leaving the old root intact. Apps without this export migrate unchecked.

#[app::migration_check]
pub fn check(old: AppV1, new: AppV2) -> bool {
// e.g. entry-count parity — see calimero_sdk::migration_check helpers
old.items_len() == new.items_len()
}

Derives Mergeable for a struct so it can be a value inside a CRDT collection (UnorderedMap<_, V>, Vector<V>, …). The generated merge() calls each field’s own merge() in declaration order, so every field must itself be Mergeable (a CRDT type or LwwRegister<T>). The same forbidden-type lint as #[app::state] applies. Enums are rejected — wrap them in LwwRegister<MyEnum> for last-write-wins, or implement Mergeable by hand.

use calimero_storage::collections::{Counter, LwwRegister};
#[derive(app::Mergeable)]
pub struct TeamStats {
points: Counter,
name: LwwRegister<String>,
}

See apps/team-metrics-macro (derive) versus apps/team-metrics-custom (hand-written Mergeable).

MacroExpands to
app::log!("processing {}", id)a runtime log call (format-string like println!)
app::err!("not found")Err(app::Error::new(..))
app::bail!("not found")return Err(app::Error::new(..))