Build a host
In this tutorial you will build an executable handl host from scratch — a Rust binary that creates an empty Host, registers a plugin that provides a capability, loads the composition, and runs it.
What you will learn
- How a
Hoststarts as an empty composer. - How plugins provide capabilities.
- How
loadresolves the plugin graph. - How to execute a capability.
Prerequisites
- A Rust toolchain.
- Follow Set up the platform first, or just create a fresh crate — the only dependency you need is the public
handl-platformcrate from thehostrepo.
Step 1: Create a new binary crate
cargo new --bin my-host
cd my-hostStep 2: Depend on the platform
[dependencies]
handl-platform = { git = "https://github.com/handl-lang/host" }
serde_json = "1"handl-platform provides DefaultHost, Plugin, CapabilityRequest, and the composition machinery. Nothing else is required — the host starts empty.
Step 3: Write a capability plugin
Replace src/main.rs with a plugin that answers a greet capability:
use handl_platform::{
CapabilityId, CapabilityMetadata, CapabilityRequest, CapabilityResponse,
DefaultHost, PlatformError, Plugin, PluginContext,
};
use serde_json::json;
struct GreetPlugin {
capabilities: Vec<CapabilityId>,
}
impl GreetPlugin {
fn new() -> Self {
Self { capabilities: vec!["greet".into()] }
}
}
impl Plugin for GreetPlugin {
fn name(&self) -> &str { "greet" }
fn capabilities(&self) -> &[CapabilityId] { &self.capabilities }
fn metadata(&self, _cap: &CapabilityId) -> Option<CapabilityMetadata> { None }
fn run(
&self,
request: &CapabilityRequest,
_ctx: &mut PluginContext,
) -> Result<CapabilityResponse, PlatformError> {
let name = request
.payload
.get("name")
.and_then(|v| v.as_str())
.unwrap_or("world");
Ok(CapabilityResponse(json!(format!("Hello, {name}!"))))
}
}
fn main() -> Result<(), PlatformError> {
let mut host = DefaultHost::new();
host.register_plugin(GreetPlugin::new());
host.load()?;
let resp = host.run(&CapabilityRequest {
capability: "greet".into(),
payload: json!({"name": "alice"}),
})?;
println!("{}", resp.0);
Ok(())
}Notice that the host starts empty. Everything it can do is added by plugins — here, a single plugin provides the entire greet capability in run.
Step 4: Build and run
cargo runYou should see:
"Hello, alice!"What happened
DefaultHost::new()created an empty composer.register_pluginaddedGreetPluginto thePluginRegistry, declaring thegreetcapability.host.load()resolved plugin dependencies and ran every plugin'scontributemethod to a fixed point (this plugin contributes nothing).host.rundispatched theCapabilityRequestto the plugin providinggreet—GreetPlugin— which returned aCapabilityResponse.
The same shape scales to real tools: kits register many plugins, plugins contribute rules to each other's RuleAcceptors, and the host dispatches capabilities without knowing what tool it has become.
Next steps
- In Write a plugin, you will add a rule-contribution path with
contribute::<P>— the type-safe way plugins extend operations. - In How to build a host, you will see a condensed recipe with fewer explanations.
- Read The composable host to learn why the host is designed this way.