Getting started: build a microapp in 1 hour
This walks the first hour of building a nexo microapp end to end. Goal: by the end of this page you have a working hello-world microapp running against a local nexo daemon, with one tool the LLM can call.
For the language-agnostic protocol spec, see
contract.md. For the full Rust SDK reference,
see rust.md. For a complete, shipping example —
React UI + HTTP backend over the admin RPC + firehose SSE,
consuming the @lordmacu/nexo-microapp-ui-react theme preset —
see lordmacu/agent-creator-microapp
and its write-up in the agent-creator reference microapp.
Prerequisites
✅ Rust 1.80+ (`rustup default stable`)
✅ The `template-microapp-rust/` directory (from a `git clone` of
nexo-rs, or copied out — it depends on `nexo-microapp-sdk` from
crates.io, so the copy builds standalone)
✅ A configured nexo daemon (one agent, one channel binding)
You don't need crates.io publish keys, npm, or a CI pipeline. Local files only.
Step 1 — copy the template (5 min)
# From your work directory (a `git clone` of nexo-rs gives you the
# template under extensions/):
cp -r /path/to/nexo-rs/extensions/template-microapp-rust ./mi-microapp
cd ./mi-microapp
# Rename inside Cargo.toml + plugin.toml + src/main.rs:
sed -i 's/template-microapp-rust/mi-microapp/g' Cargo.toml plugin.toml src/main.rs
git init && git add -A && git commit -m "scaffold from nexo template"
# Sanity-check it builds (no path-dep surgery needed — the SDK
# resolves from crates.io):
cargo build
Now you have:
mi-microapp/
├── Cargo.toml # depends on nexo-microapp-sdk = "0.1" (crates.io)
├── plugin.toml # capabilities + transport declaration
├── README.md # rename checklist + porting guide
└── src/main.rs # ~100 LOC including comments
Step 2 — write your first tool (15 min)
Open src/main.rs. Replace the greet_tool body with your
domain logic:
#![allow(unused)] fn main() { async fn buscar_cliente(args: Value, ctx: ToolCtx) -> Result<ToolReply, ToolError> { let phone = args .get("phone") .and_then(|v| v.as_str()) .ok_or_else(|| ToolError::wire("phone required"))?; // BindingContext threads the agent + channel + account // (Phase 82.1) through every call. let agent = ctx.binding().map(|b| b.agent_id.clone()).unwrap_or_default(); Ok(ToolReply::ok_json(json!({ "agent": agent, "phone": phone, "found": false, "lead_id": null, }))) } }
Register it in main():
#![allow(unused)] fn main() { let app = Microapp::new("mi-microapp", env!("CARGO_PKG_VERSION")) .with_tool("mi_microapp_buscar_cliente", buscar_cliente); }
Build:
cargo build --release
The binary lands in ./target/release/mi-microapp.
Step 3 — smoke test the wire (5 min)
The microapp speaks line-delimited JSON-RPC over stdio. You can exercise it without the daemon:
echo '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
| ./target/release/mi-microapp
Expected output (one line, JSON):
{"jsonrpc":"2.0","id":1,"result":{
"tools":["mi_microapp_buscar_cliente"],
"hooks":["before_message"],
"server_info":{"name":"mi-microapp","version":"0.1.0"}
}}
tools/call works the same way:
printf '%s\n%s\n' \
'{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \
'{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"mi_microapp_buscar_cliente","arguments":{"phone":"+57311"}}}' \
| ./target/release/mi-microapp
If both calls return clean JSON, your microapp speaks the contract.
Step 4 — install into the daemon (15 min)
Copy the build artifact + plugin.toml into the daemon's
extensions/ directory:
mkdir -p ~/.nexo/extensions/mi-microapp
cp target/release/mi-microapp ~/.nexo/extensions/mi-microapp/
cp plugin.toml ~/.nexo/extensions/mi-microapp/
Reference the microapp from ~/.nexo/config/extensions.yaml:
extensions:
entries:
mi-microapp:
enabled: true
capabilities_grant:
- dispatch_outbound # if your tools call nexo/dispatch
# add more as your microapp needs them
Reference its tool from ~/.nexo/config/agents.yaml:
agents:
- id: ana
extensions: [mi-microapp]
allowed_tools:
- mi_microapp_buscar_cliente # appears in the LLM tool catalogue
Restart the daemon:
nexo daemon restart
# or for dev: kill the process and re-run `nexo daemon start`
Step 5 — verify the LLM sees your tool (10 min)
Send a test message through your bound channel. The LLM should
see mi_microapp_buscar_cliente in its tool catalogue and call
it on relevant prompts.
Check the daemon logs:
nexo logs --tail | grep mi-microapp
You should see:
extensions: spawned mi-microapp pid=...extensions: mi-microapp -> initialize oktools/call mi_microapp_buscar_cliente {"phone": "..."}
If the tool is being called but the LLM doesn't surface it
correctly, the prompt may not have descriptions rich enough —
add a description to your tool registration.
Step 6 — add per-agent config (10 min)
Different agents may need different microapp behaviour. Use
Phase 83.1 (see proyecto/PHASES.md) extensions_config:
agents:
- id: ana
extensions: [mi-microapp]
extensions_config:
mi-microapp:
regional: bogota
api_token_env: ANA_ETB_TOKEN
- id: maria
extensions: [mi-microapp]
extensions_config:
mi-microapp:
regional: cali
api_token_env: MARIA_ETB_TOKEN
In your handler, the BindingContext.agent_id lets you key
into a per-agent config map you build at initialize time.
Until 83.1.b ships the JSON-RPC propagation, the operator can
also pass the config via env vars and your microapp reads them
on boot.
Common patterns
Multi-tenant SaaS
You're shipping a single microapp binary that serves multiple
tenants. See extensions/multi-tenant-saas.md.
Key idea: every tool call carries BindingContext.account_id
(Phase 82.1) — key your per-tenant SQLite tables on it.
Compliance enforcement
Drop in nexo-compliance-primitives
to anti-loop / anti-manipulation / opt-out / PII-redact / rate
limit / consent track. Wire each primitive into a Phase 83.3
hook that votes Block or Transform before the LLM sees
the inbound.
Outbound dispatch
Need your microapp to send a WhatsApp / Telegram / email reply?
Use the nexo-microapp-sdk outbound feature:
[dependencies]
nexo-microapp-sdk = { path = "...", features = ["outbound"] }
Then ctx.outbound().dispatch(...) from inside any tool
handler. See extensions/stdio.md.
Troubleshooting
| Symptom | Fix |
|---|---|
extensions: mi-microapp -> initialize timed out | Microapp didn't reply within 30 s. Check stderr; missing tokio runtime is the most common cause. |
tool 'mi_microapp_x' not in catalogue | Tool name missing the <extension_id>_ prefix. Daemon enforces the namespacing. |
capability denied: dispatch_outbound | Operator forgot to add the capability to extensions.yaml.entries.<id>.capabilities_grant. |
404 unknown method: hooks/before_message | The hook name in your with_hook(...) call doesn't match a daemon-emitted hook. Check crates/extensions/src/runtime/mod.rs::HOOK_NAMES. |
Build fails: nexo-microapp-sdk = "0.1" not found | SDK isn't on crates.io yet (Phase 83.14). Use path = "..." against your nexo-rs checkout. |
Next steps
You have a working microapp. Now:
- Read contract.md end-to-end — the wire spec is short, and every detail matters for compat.
- Read rust.md for the full SDK reference.
- For multi-tenant SaaS: extensions/multi-tenant-saas.md.
- For compliance gating: pull in
nexo-compliance-primitivesand wire its primitives into your Phase 83.3 hooks.