Your first deployment

Ten minutes, no devices, no containers. We will deploy two toy components with a dependency between them, watch them run, kill one, and watch Keystone put it back.

1. A working directory

The agent keeps its state and installed components under runtime/ in whatever directory you start it from, so give it its own.

mkdir -p ~/keystone-demo/recipes && cd ~/keystone-demo

2. Two recipes

recipes/clock.toml — a component that just keeps running:

[metadata]
name = "clock"
version = "1.0.0"

[lifecycle.run]
restart_policy = "always"

[lifecycle.run.exec]
command = "/bin/sh"
args = ["-c", "while true; do date; sleep 5; done"]

recipes/reporter.toml — a component that depends on the first:

[metadata]
name = "reporter"
version = "1.0.0"

[lifecycle.run]
restart_policy = "always"

[[dependencies]]
name = "clock"

[lifecycle.run.exec]
command = "/bin/sh"
args = ["-c", "while true; do echo reporting; sleep 7; done"]

The [[dependencies]] block refers to the recipe name clock. Keystone maps it to whichever component in the plan uses that recipe.

3. A plan

plan.toml:

[[components]]
name = "clock"
recipe = "recipes/clock.toml"

[[components]]
name = "reporter"
recipe = "recipes/reporter.toml"

4. Start the agent

keystone --http 127.0.0.1:8080 --insecure-skip-verify
Warning

--insecure-skip-verify turns off the mandatory artifact signature checks. These recipes have no artifacts to verify, but the flag keeps the demo friction-free. Never use it on a real device — see Secure defaults.

5. Apply the plan

In another terminal:

curl -X POST --data-binary @plan.toml http://127.0.0.1:8080/v1/plan/apply

The agent uploads and parses the plan, resolves the graph, and starts clock first, then reporter. The log tells the story:

[agent] reconcile plan stop_order=[] start_order=[clock reporter] no_touch=[]
[supervisor] layer=0 components=[clock] msg=starting layer
[supervisor] component=clock state=running
[supervisor] layer=1 components=[reporter] msg=starting layer
[supervisor] component=reporter state=running
[supervisor] all components running

6. Look at what is running

curl -s http://127.0.0.1:8080/v1/components | jq
[
  { "name": "clock",    "state": "running", "restarts": 0, "pid": 40211, "last_health": "unknown" },
  { "name": "reporter", "state": "running", "restarts": 0, "pid": 40219, "last_health": "unknown" }
]

last_health is unknown because neither recipe declares a health check — that is normal, not a problem. See Health checks.

7. Break something

kill -9 40211        # the clock's PID
sleep 3
curl -s http://127.0.0.1:8080/v1/components | jq '.[0]'
{ "name": "clock", "state": "running", "restarts": 1, "pid": 41002 }

New PID, restarts: 1. The restart policy was always, so the runner brought it back. Nothing you had to do.

8. Re-apply the same plan

curl -X POST --data-binary @plan.toml http://127.0.0.1:8080/v1/plan/apply
curl -s http://127.0.0.1:8080/v1/components | jq '.[].pid'

The PIDs do not change. Applying an unchanged plan is a no-op: Keystone reconciles rather than restarts. This matters on a device you cannot afford to disturb. See Reconcile and reuse.

9. Stop everything

curl -X POST http://127.0.0.1:8080/v1/plan/stop

Using the CLI instead

Everything above works through keystonectl too:

keystonectl status
keystonectl components
keystonectl apply plan.toml
keystonectl restart clock
keystonectl stop-plan