Skip to content

SDK Macros

An app is a Rust library built into a calimero:app component. It declares itself with the attributes calimero-sdk re-exports under calimero_sdk::app (defined in crates/sdk-macros) and #[derive(Record)]. One import brings them all in scope:

use calimero_sdk::prelude::*;
Attribute On Does
#[app::state(version = N)] struct The state root: tables and collections.
#[app::logic] impl of the state Every pub fn becomes a method; generates the handle export and the ABI.
#[app::init] method The constructor, run once when the app is created (as the owner).
#[app::view] &self method A read: runs as a view, never as a write.
#[app::confirm] method The caller confirms with a fresh passkey assertion.
#[app::public] / #[app::public(rate = "10/min")] method Callable without signing in (on a public app), optionally rate-limited.
#[app::every("1m")] method A timer the node runs on that interval (<n>s, <n>m or <n>h).
#[app::allow(system)] method The node itself may call it (implied by every).
#[app::allow(app = "<id>")] / allow(app = "*") method Another app may call it with a cross-app call.
#[app::action] method An action: may fetch over HTTP and read and call the app, outside the log.
#[app::allow(action)] method Only the app’s own actions may call it.
#[app::error] enum The app’s errors: variant name = error code.
#[app::event] enum The app’s events: variant name = event name.
#[derive(Record)] + #[table(..)] struct A table row: key, unique constraints, indexes, counts, search.
app::bail!(err) expression Return early with an app error.

The sample guest the SDK’s own tests run (crates/sdk/tests/guests/sample), shortened:

use calimero_sdk::prelude::*;
#[derive(Record)]
#[table(
key = "id",
unique(slot = ["room_id", "start"]),
index(by_start = ["checked_in", "start"]),
count(by_room = ["room_id"]),
)]
pub struct Booking {
pub id: String,
pub room_id: String,
pub start: u64,
pub title: String,
pub checked_in: bool,
}
#[app::state(version = 3)]
pub struct Rooms {
bookings: Table<Booking>,
titles: UnorderedMap<String, String>,
next_id: Counter,
}
#[app::error]
pub enum Error {
NotFound,
Taken { start: u64 },
Invalid(String),
}
#[app::event]
pub enum Event {
Booked { id: String, room_id: String, start: u64 },
Released { id: String },
}
#[app::logic]
impl Rooms {
#[app::init]
pub fn init() -> app::Result<Self> {
Ok(Self::load())
}
pub fn book(&mut self, room_id: String, start: u64, title: String) -> app::Result<String> {
if title.is_empty() {
app::bail!(Error::Invalid("empty title".into()));
}
let id = env::unique_id();
self.bookings.insert(Booking { id: id.clone(), room_id: room_id.clone(), start, title, checked_in: false })?;
env::emit(Event::Booked { id: id.clone(), room_id, start }, Audience::All)?;
Ok(id)
}
#[app::view]
#[app::public]
pub fn room_count(&self, room_id: String) -> app::Result<u64> {
Ok(self.bookings.count("byRoom", &[KeyPart::Str(room_id)])?)
}
#[app::confirm]
pub fn admin_release(&mut self, id: String) -> app::Result<()> {
if !self.bookings.remove(&id)? {
app::bail!(Error::NotFound);
}
env::emit(Event::Released { id }, Audience::All)?;
Ok(())
}
#[app::every("30s")]
pub fn heartbeat(&mut self) -> app::Result<()> {
Ok(())
}
}

The struct holds handles only: each field is a table or collection, built from its camelCase field name, so Self::load() reads nothing. version is recorded in the ABI. The fields and their kinds (table, unorderedMap, counter, cell, set, vector, frozen, perUser, text, richText) are listed in the ABI too.

On the state’s impl block. Every pub fn is a method, named in camelCase in the ABI (room_count → roomCount); private fns stay helpers. Methods take &mut self (a call), &self with #[app::view] (a view), or nothing (#[app::init], which returns Self). Every method returns app::Result<T>.

The generated handle:

  • Arguments are a JSON object keyed by camelCase parameter names; unknown keys are refused (BadArgs).
  • 64-bit integers and bytes: u64, i64 and Vec<u8> (and Option of each) travel as decimal strings and base64url without padding, in arguments, results, record fields, and error and event fields, so no JavaScript client loses precision. The same types nested in another type the macro sees (Vec<u64>, Option<Vec<i64>>) are a compile error: wrap them in a Record or use a string.
  • Kinds: a view may run as a call; a call run as a view is NotAView; an unknown method is UnknownMethod.
  • Callers are checked after the kind: allow(system) admits the node, allow(app = ..) another app, public a caller who is not signed in.
  • #[app::init] runs once, as the owner, and takes no other attribute.
  • #[app::view] makes the method a read.
  • #[app::confirm] asks the node to require a fresh passkey assertion for the call.
  • #[app::public] lets a caller who is not signed in reach the method on a public app; rate = "<n>/s|min|h" limits it.
  • #[app::every("<n>s|m|h")] is a timer. It implies allow(system). Inside it, env::timer() gives the tick (scheduled, and previous when there was one).
  • #[app::allow(app = "<app id>")], or "*" for any app, admits cross-app calls.
  • #[app::action] takes no self; its first parameter is ctx: &mut ActionCtx, with ctx.view, ctx.call and ctx.fetch (see Outbound). #[app::allow(action)] on a method lets only the app’s own actions call it.

Both go in the #[app::logic] module, at most one of each.

  • #[app::error] implements From<Error> for AppError. The variant name is the code; the fields become the error’s message, a JSON object with camelCase keys (Taken { start: 7 } → code Taken, {"start":"7"}).
  • #[app::event] implements AppEvent, emitted with env::emit(event, audience) (see Events).

app::bail!(Error::NotFound) returns Err(Error::NotFound.into()).

A table row. The derive writes the serde impls itself, so do not also derive Serialize/Deserialize on the record.

Key Meaning
key = "field" The primary key.
unique(name = ["f", ..]) A unique constraint; an insert that breaks it fails.
index(name = ["f", ..]) An index, read with Table::find.
count(name = ["f", ..]) A count kept on every write, read with Table::count.
search = ["f", ..] Fields indexed for full-text search.

Index, count and unique names are used in camelCase (by_room → "byRoom"). u64/i64 fields are CBOR integers when stored and decimal strings in JSON.

The crate is calimero-sdk. Its old name, calimero-app-sdk, still works while apps move over: it re-exports calimero-sdk, and the attributes expand to paths through whichever name the app’s Cargo.toml lists (or the name a renamed dependency gives it).