Introduction
Getting started
This guide takes you from nothing to a running durable workflow. It assumes
you have installed the toolchain: the hop CLI built
from this repository, plus a Rust toolchain with the
wasm32-unknown-unknown target for the sample guest.
Every command below works today. Where something is still rough, the page says so.
Two commands are worth knowing up front. hop doctor checks your machine
against everything below (the wasm32 targets especially) and prints the
command that fixes anything missing. hop on its own (or hop status)
tells you where you are: what this project points at, what is built, what
is deployed, whether anything is listening, and the single next command for
the state it finds. Run it whenever you lose your place.
0. Or start from a scaffold
If you would rather begin from your own project than from this repository’s
sample, hop init writes one, in whichever guest language you work in:
hop init ./greetings --lang rust # or typescript, python, go, haskell
Each language gets the project shape it naturally has: a cdylib crate for
Rust, one entry module for TypeScript, an import-free script for Python, a
package plus go.mod for Go, and a Cabal package with a unit test for
Haskell. The generated project already points at the guest SDK, and the
command prints the exact hop build / hop run lines for what it wrote.
The rest of this guide applies unchanged.
The generated workflow awaits nothing, which is what lets it run against
hop dev alone. That is not the whole product (awaiting an activity is the
point of durable execution), but an activity needs a second process to
serve it, as explained below, and a scaffold that hung on the first hop run would be a poor first experience. Each generated file shows the
activity version of itself in a comment, right under the code.
Activities need a worker, and hop dev is not one
Know this before you add your first activity. An activity is dispatched
under its own task type, hopskip:activity.<name>@1, and is executed by a
worker registered for that task type, which you write with
core/hopskip-worker (Rust) or sdk/<lang>/hopskip-worker. hop dev runs
a workflow worker: it serves the task types your deployments declare.
If nothing serves an activity, the dispatch is accepted, durably recorded,
and then waits, correctly, forever. The CLI tells you instead of leaving you
to guess: hop dev warns at startup when a workflow it is serving
dispatches activities, and hop run explains the silence after it has been
waiting fifteen seconds (--timeout <seconds> stops watching; the workflow
keeps running).
Once you have more than one workflow, point the build at the directory rather than a file, in any language, or a mix of them:
hop workflows ./my-workflows # what's here, and under what task types?
hop build --manifest-path ./my-workflows
hop dev --watch ./my-workflows # rebuild + redeploy on every save
Each workflow is found by its own language’s convention and named from its
own source, so there is no --task-type to invent and no second command per
workflow. @hopskip task-type: in a comment overrides the derived name.
1. Build a workflow
The repository ships a minimal, activity-free guest,
sdk/rust/hello-workflow, written against the Rust SDK:
hopskip_sdk::workflow!(greet);
async fn greet(name: String) -> String {
let name = if name.is_empty() { "world".to_owned() } else { name };
format!("Hello, {name}! This greeting is durable: ...")
}
hop build compiles it to a sandboxed Wasm module, extracts its
control-flow graph, and registers it in the local content-addressed registry
(.hop/registry). The --task-type flag binds the dispatch task type: the
string clients use to start the workflow and workers use to serve it.
hop build \
--manifest-path sdk/rust/hello-workflow/Cargo.toml \
--task-type hopskip:hello.greet@1
# ==> cfg_fingerprint = ... (N function(s), ...)
# ==> registry entry written: .hop/registry/blobs/<hash>/entry.json
# <hash>
The final line is the module’s content hash, its immutable identity.
2. Deploy it
hop deploy points a namespace at the built blob. When the namespace was
already deployed, the compatibility gate first diffs the new build’s
control-flow graph against the currently deployed one and blocks
replay-breaking changes (there is nothing to diff on a first deploy):
hop deploy <hash> --namespace default
# ==> namespace 'default' now deployed at component_hash = <hash>
3. Start the local stack
hop dev runs everything in one process: a single-node hopskip-server with
a durable on-disk log store, a worker serving every registry deployment that
has a task-type binding, and the console HTTP API:
hop dev
# ==> deployment: namespace 'default' routes new starts to <hash>
#
# hop dev is up:
# gRPC (clients + workers) http://127.0.0.1:50051
# console API http://127.0.0.1:8090
# console UI cd frontend && npm run dev
# log store .hop/log
#
# environment for other terminals:
# export HOPSKIP_SERVER_ADDR=http://127.0.0.1:50051
# export HOPSKIP_TOKEN=...
# export HOPSKIP_DLQ_TOKEN=...
Everything is durable by default: the event log lives under .hop/log and
survives restarts. hop dev also mints two bearer tokens against a
per-checkout dev secret: a client token (HOPSKIP_TOKEN) and an operator
token (HOPSKIP_DLQ_TOKEN) for the admin surfaces.
4. Run it
In a second terminal (after exporting the environment hop dev printed):
hop run hopskip:hello.greet@1 --input Ada
# ==> started client-1 (hopskip:hello.greet@1 in namespace 'default')
# ==> awaiting result...
# header content-type: text/plain; charset=utf-8
# ==> completed: Hello, Ada! This greeting is durable: ...
Here is what happened: the client’s StartWorkflow was routed to the
deployed blob (hop deploy set the routing pointer), the server pushed a
task to the worker over its persistent stream, the worker drove a sandboxed
Wasm execution of your module, and the result was committed to the
hash-chained event log before GetResult returned it.
5. Look around
- Console: with
hop devrunning, runcd frontend && npm install && npm run dev, then openhttp://127.0.0.1:4390. The console reads live data from the console API (workflows, workers, triage, audit views). - Visibility: read a workflow’s projected log entries with
hop logs default:<workflow>. Current@searchablestate is served by the console API’s typed routes; see the visibility guide. - Audit proof: fetch a workflow’s Merkle history and write an inclusion
proof:
hop audit default:<workflow> --token $HOPSKIP_DLQ_TOKEN. - Replay: re-execute a registered module deterministically with
hop replay <hash>, optionally strictly against a recorded history file (--history).
Current edges
Accurate as of this page’s last update:
- Workflows that invoke activities dispatch each activity as its own
task (task type
hopskip:activity.<name>@1), which needs a worker registered for that task type.hop devis one: it serves whatever the registry deploys, so an activity built and deployed under its own task type is served alongside the workflow that calls it. Activities get concurrency slots of their own, so a workflow waiting on one never holds the slot it needs. For a standalone worker, seesdk/rust/dogfood-worker. A generic activity-worker harness is on the roadmap. - The five-language SDK matrix (Rust, TypeScript, Python, Haskell, Go) is conformance-tested in CI, but the non-Rust build toolchains are provisioned for macOS arm64 in-tree today; use the Nix dev shell for those.
- All local traffic is plaintext gRPC with dev-secret bearer auth. Do not
expose
hop devbeyond localhost.
Next steps
- Writing workflows
- Activities & retries
- CLI reference: the full command surface.