Tutorials
Four end-to-end walkthroughs that exercise the most-requested XEM workflows. Copy the commands and payloads verbatim, substitute your own project external ID, workspace ID, and agent names. Assumes you finished Deploy Your First XEM.
1. Author Your First Chat Template (drain-node)
Goal: ship a chat template that encodes the drain-node runbook and proves the operational safety invariants during a live run.
Step 1, seed a file payload from the platform template. The CLI has no copy verb; export the existing template as JSON, rename it, and save it locally.
xeroctl templates show drain-node -o json \
| jq '.name = "drain-node-acme"' \
> drain-node-acme.json
Step 2, edit the template. Open
drain-node-acme.json. The file payload uses these
top-level fields:
{
"name": "drain-node-acme",
"system_prompt": "...",
"description": "Drain a Kubernetes node safely.",
"allowed_tools": ["kubectl_get", "kubectl_pdb_list",
"kubectl_node_cordon", "kubectl_node_drain",
"kubectl_node_uncordon", "kubectl_pod_list"],
"approval_policy": "elevated",
"applicable_workspace_types": ["kubernetes"]
}
Customize the system_prompt: the procedure should
enumerate pre-conditions (get pods, check PDBs, identify
DaemonSet pods), the cordon/drain/verify sequence, and the
uncordon rollback on failure. Do not allow tools outside
allowed_tools. The approval_policy
enum is standard, elevated, or
restricted.
Step 3, create the template at project scope.
xeroctl templates create --file drain-node-acme.jsonStep 4, apply the template to a workspace. Applying opens a chat against the workspace pinned to the current template version:
xeroctl templates apply drain-node-acme --workspace kubernetes-stagingIn the returned chat, ask to drain a test node; approve each gated tool call; verify the final summary says every pod was evicted and no PDBs were violated.
Step 5, promote to platform scope. Once the template has run cleanly in staging, promote it so every project in the deployment can apply it:
xeroctl templates promote drain-node-acme --scope platform
The promote verb only accepts
--scope platform; workspace-to-workspace copy is
not a supported operation. To run the template in a second
workspace, call xeroctl templates apply against
that workspace.
2. Deploy XEM on a Jumphost
Goal: run a XEM on a locked-down jumphost via docker compose, with credentials mounted read-only and a systemd supervisor.
Start from the published
compose.agent-xem.yaml in the
xerotier/container-agents
repository (see
Deploy Your
First XEM for the base setup and the
compose/.env), then layer a hardening
override and a systemd supervisor on top of it. Target
credentials (kubeconfig, AWS) go in the base file's
read-only /data/xerotier-xem/credentials
mount.
Hardening override. Save alongside the
base file as compose/compose.override.yml.
Docker Compose merges it onto the base
xem-agent service: read-only root
filesystem, all capabilities dropped, no privilege
escalation, and a writable tmpfs.
services:
xem-agent:
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
Systemd supervisor. Save as
/etc/systemd/system/xerotier-xem.service so
the stack returns after a reboot.
WorkingDirectory points at the cloned
compose/ directory so both files and the
.env resolve.
[Unit]
Description=Xerotier XEM (docker compose)
After=docker.service network-online.target
Requires=docker.service
Wants=network-online.target
[Service]
Type=oneshot
RemainAfterExit=yes
WorkingDirectory=/opt/xerotier-public/compose
ExecStart=/usr/bin/docker compose -f compose.agent-xem.yaml -f compose.override.yml up -d
ExecStop=/usr/bin/docker compose -f compose.agent-xem.yaml -f compose.override.yml down
TimeoutStartSec=0
[Install]
WantedBy=multi-user.target
Bootstrap sequence. Mint a join key and
set it (with the router URL and registration name) in
compose/.env, then enable the unit.
xeroctl agents join-keys --create \
--name xem-jumphost-01 \
--region us-east-1 \
--router-addr tcp://router.example.com:5555 \
-o json | jq -r '.join_key'
# Put the printed token in compose/.env as XEROTIER_AGENT_JOIN_KEY=...
sudo systemctl daemon-reload
sudo systemctl enable --now xerotier-xem
journalctl -u xerotier-xem -f
xeroctl agents list --region us-east-1
--name, --region, and at least one
--router-addr are required by the join-key
creation endpoint; the agent tier is determined at
enrollment by the agent binary, not by a flag on the join
key.
The unit is now supervised. Docker-compose restart policy handles process-level crashes; systemd handles boot-time recovery; the router re-admits the XEM on reconnect using its CURVE key.
3. Connect a Slack Bot to Approval Webhooks
Goal: deliver pending approvals to a Slack channel, let reviewers approve/reject from a Slack message, and round-trip the decision back to the router.
Step 1, create a Slack app. In
api.slack.com/apps,
create an app, enable Incoming Webhooks for a channel, and
enable Interactivity with a request URL pointing at your
bot service (for example
https://bots.example.com/slack/interact).
Note the signing secret.
Step 2, register a webhook endpoint with the router.
xeroctl webhooks --create \
--url https://bots.example.com/approvals \
--events 'exec.approval_requested,exec.approval_timed_out' \
--secret "$(openssl rand -hex 32)"
Webhook subscription is a flat command, there is no
endpoints subgroup. The signing secret is
supplied via --secret; generate one locally
(for example with openssl rand -hex 32) and
store it alongside the bot service so signature verification
can find it.
Step 3, handle exec.approval_requested.
On receipt, verify the X-Webhook-Signature
header against the signing secret, then post a Slack message
with Approve / Reject buttons. The button value carries the
approval ID.
Step 4, handle the Slack interaction. Verify Slack's signature. Read the button value to get the approval ID, then call the router:
POST /:project_id/v1/exec/approvals/:id/approve
Authorization: Bearer <bot-api-key>
Content-Type: application/json
{
"note": "Approved via Slack by <user>"
}
The router validates that the bot's API key carries the
execution scope and that the user's delegation
includes approval rights on the workspace.
Step 5, close the loop. The router does
not emit an approval-resolved webhook today; once the bot's
POST to /approve (or /reject)
returns 200, update the Slack message with the final
decision and disable the buttons to prevent double-actions.
Subscribe to exec.approval_timed_out to handle
the timeout case from the router side.
See Webhook Events for the full payload schema.
4. Build a Custom Tool
Goal: ship a site-specific tool via
/var/lib/xerotier/tools/custom/, no XEM
rebuild needed.
Step 1, write the tool manifest. Save as
/var/lib/xerotier/tools/custom/restart_cache.json:
{
"name": "restart_cache",
"description": "Restart a named Redis cache cluster.",
"risk": "destructive",
"idempotent": false,
"timeout_seconds": 60,
"parameters": {
"type": "object",
"required": ["cluster"],
"properties": {
"cluster": {
"type": "string",
"pattern": "^[a-z0-9-]{1,63}$"
},
"reason": {"type": "string"}
}
},
"command": {
"argv": ["/usr/local/bin/restart-cache.sh",
"--cluster", "",
"--reason", ""],
"stdin": null,
"env": {},
"cwd": "/srv/ops"
}
}
Step 2, supply the executable.
/usr/local/bin/restart-cache.sh is a shell
script the XEM host owns; it exits 0 on success and writes
a JSON summary to stdout.
Step 3, let the XEM pick up the manifest. The XEM watches its tools directory and reloads on file change, no explicit reload command is needed. After the file is in place, confirm the agent re-registered the tool on its next heartbeat:
xeroctl agents <agent-id> -o json | jq '.tools[] | select(.name == "restart_cache")'
The router accepts the updated manifest on the next heartbeat;
restart_cache is now invokable from any workspace
bound to the XEM.
Step 4, exercise the tool.
xeroctl exec invoke \
--workspace cache-prod \
--tool restart_cache \
--args '{"cluster":"sessions-primary","reason":"memory-leak"}'The router gates the call behind the workspace's approval policy (destructive risk). After approval, the XEM shells out to the script; the summary returns as the tool result.
Security reminder: every custom tool
inherits the XEM host's filesystem access. Review the
argv, env, and cwd for
each tool; treat the custom tools directory as privileged
configuration and version-control it.