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 dev running, run cd frontend && npm install && npm run dev, then open http://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 @searchable state 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 dev is 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, see sdk/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 dev beyond localhost.

Next steps