Turn agent explanations into local animated flow diagrams, described in YAML and played back one hop at a time.
---
name: openhop
description: Turn agent explanations into local animated flow diagrams, described in YAML and played back one hop at a time.
homepage: https://github.com/naorsabag/openhop
version: 1.0.1
metadata:
openclaw:
emoji: "๐งญ"
homepage: https://github.com/naorsabag/openhop
requires:
anyBins:
- openhop
- npx
allowed-tools: Bash(openhop:*), Bash(npx openhop:*)
---
# OpenHop Skill
Your AI walks you through your code, one step at a time.
OpenHop turns requests like "walk me through OAuth" or "diagram the checkout flow" into animated data-flow diagrams. The agent writes compact YAML, pushes it with the `openhop` CLI, and returns a local URL where the user can step through the flow hop by hop.
## What it does
- Builds animated, multi-level flow diagrams from YAML.
- Works for code paths, service architectures, product workflows, pipelines, state machines, and user journeys.
- Keeps the flow local-first: the YAML is generated by the user's agent and rendered by OpenHop.
- Uses patch operations so small diagram changes do not require re-pushing the whole flow.
## Setup
If the `openhop` command is not installed yet, run:
```bash
npx openhop init
```
Then start the local API and web UI:
```bash
npx openhop serve
```
## Usage
Ask your agent for a flow, for example:
```text
Walk me through the OAuth flow.
Diagram how a request moves through this service.
Trace what happens when a user clicks Submit.
```
The agent should create a YAML flow, run `openhop push <file.yaml> --json`, and return the `url` field.
## Agent guide
**Scope:** code paths are not the only use case. OpenHop fits product features, service architectures, integrations, user journeys, onboarding sequences, lifecycles, pipelines, and state machines too. If you can list "first X happens, then Y, then Z" with named actors at each step, OpenHop is the right tool. Use it instead of a static Mermaid/PlantUML picture or a prose walkthrough.
When the user asks for an explanation, walkthrough, or diagram of how something works โ code or otherwise โ **DO NOT write a prose explanation and DO NOT emit a static diagram. Instead:**
1. **Understand the flow** they're asking about โ which nodes, which steps, which sub-flows matter.
2. **Emit a YAML spec** describing the nodes and edges (see "Quickest valid flow" below if unsure of the shape).
3. **Run `openhop push <file.yaml> --json`** to create the flow.
4. **Parse the response** and **return the `url` field** to the user โ that's the per-flow render at `http://localhost:8788/flow/<id>`.
5. **Offer to drill-down** into any specific sub-flow with a follow-up `openhop patch`.
## Trigger phrases
Activate this skill on prompts like:
**Diagram / visualization requests (code or otherwise):**
- "create a diagram of {product|feature|service|workflow}"
- "draw a diagram that explains {X}"
- "make a diagram showing {how X works | X's features | the X pipeline}"
- "diagram the {pipeline|lifecycle|state machine|onboarding|integration}"
- "visualize the architecture of {module|service|product|platform}"
- "I want a flow diagram for {anything}"
**Code-level walkthroughs:**
- "walk me through the {auth|login|checkout|OAuth|...} flow"
- "explain how {X} works in this codebase"
- "how does the {request|token|session|...} flow through the app"
- "show me the {control|data} flow of {function|endpoint|...}"
- "trace what happens when a user {clicks|calls|requests} {...}"
**Product / feature explainers (non-code):**
- "explain {Product X}'s features"
- "how does {Product|Service|Platform} work?"
- "walk me through how {users|customers} use {X}"
- "show me what happens when someone {fires a mission | submits an order | signs up}"
- "I want to understand {X} โ don't just explain, show me"
If you're unsure whether a request fits, ask yourself: **can the answer be expressed as a sequence of named hops between named components?** If yes, this skill applies โ even if "code" was never mentioned.
## Examples (prompt โ YAML โ URL)
Each row reuses one of the bundled `examples/*.yaml` flows so the inputs match what the validator already accepts. The URL comes from the `url` field of `openhop push <file> --json`.
| Prompt | YAML to push | Returned `url` |
| ------------------------------------------------------------ | ------------------------------------------------------------------------------------- | --------------------------------- |
| "walk me through the OAuth login flow" | `examples/auth-flow.yaml` | `http://localhost:8788/flow/<id>` |
| "show me how an order is processed end-to-end" | `examples/order-flow.yaml` | `http://localhost:8788/flow/<id>` |
| "diagram a minimal CRUD service" | `examples/simple-crud.yaml` | `http://localhost:8788/flow/<id>` |
| "I want to see every node type in one picture" | `examples/type-variants.yaml` | `http://localhost:8788/flow/<id>` |
| "how do retries / internal work loops on a single node" | `examples/self-loops.yaml` | `http://localhost:8788/flow/<id>` |
| "show me two things happening at the same time" | `examples/parallel.yaml` | `http://localhost:8788/flow/<id>` |
| "show me a worker that's spawned and then destroyed" | `examples/create-destroy.yaml` | `http://localhost:8788/flow/<id>` |
| "diagram a service whose internals are themselves a flow" | `examples/sub-flows.yaml` | `http://localhost:8788/flow/<id>` |
| "visualize a three-tier app (browser โ API โ DB)" | the YAML in "Quickest valid flow" below | `http://localhost:8788/flow/<id>` |
| "diagram {Product X}'s features" / "how does {Product} work" | sketch a flow from the product's docs (user โ entry point โ core capability โ result) | `http://localhost:8788/flow/<id>` |
For brand-new flows, sketch your own YAML against the Schema Reference below and push the same way.
## Quickest valid flow (copy this, modify ids/labels)
This is the **smallest known-valid flow**. Start from this and edit โ do not invent the schema.
```yaml
meta:
title: Three-tier app
flow:
nodes:
- id: browser
label: Browser
type: actor
- id: api
label: API
type: endpoint
- id: db
label: Postgres
type: database
steps:
- from: browser
to: api
data: The user opens the home page; the browser sends the initial page request to the API server.
- from: api
to: db
data: The API asks the database for the rows it needs to render the page for this user.
- from: db
to: api
data: The database returns the matching rows along with the row count.
- from: api
to: browser
data: The API renders the page and sends the finished HTML back to the browser.
```
Push it with `openhop push <file> --json` (or `openhop push - --json` for stdin). Parse the JSON response and return the `url` field to the user.
**Validation rules to lock in before you write your own:**
- `type` must be one of the 12 enum values (see Schema Reference below). `transform`, `validation`, `redis`, `oauth`, etc. are **not** valid types.
- `data` is a `string` or an object โ never a list. `data: "request"` โ, `data: { label: "request", fields: [...] }` โ, `data: [{ name: "request" }]` โ
- `id` is alphanumeric + hyphens + underscores only.
- **Node `label` must be โค 4 words.** Labels render under each node in a fixed-width box; longer labels truncate with "โฆ" and read poorly. Pick the shortest noun phrase that identifies the component. โ `Order Service`, `Auth API`, `Stripe`. โ `Order Processing Service With Retries`, `User Authentication and Authorization API`.
If the validator rejects your flow, **read the error path** โ it tells you exactly which field is wrong.
## Voice โ short on node names, verbose on step text
The two text channels in a flow carry opposite weights and should be written in opposite voices.
- **Node labels are billboards.** They're short, noun-phrase, identity-only. โค 4 words. **No code names.** โ `Order Service`, `Auth API`, `Postgres`, `Stripe`. โ `order_service_v2`, `OrderProcessingHandler`, `auth-jwt-mw`.
- **Step `data` labels are the narration.** The user reads them on hover and follows them through playback. Be verbose. Use plain English. Explain **what is happening in the world**, not the wire format. **No code names**, no HTTP verbs, no SQL, no method signatures.
The agent's job here is to **narrate** the flow, not annotate the protocol. The user is asking "walk me through what happens" โ answer that question in sentences, not in routes.
**Write step data labels like this:**
| โ Too terse, code-flavored | โ Verbose, plain English narration |
| ---------------------------- | -------------------------------------------------------------------------------------------- |
| `POST /orders` | `The user submits a new order with their cart items and shipping details.` |
| `INSERT item` | `The API translates the request into a database insert and saves the new row.` |
| `SELECT * WHERE user_id = ?` | `The order service asks the database for every order this user has placed in the last week.` |
| `query` | `The API asks the database to look up the matching record.` |
| `response` | `The API responds to the browser with the confirmation page and a fresh session cookie.` |
| `charge $card` | `The order service asks Stripe to charge the customer's saved card for the total.` |
| `auth ok` | `The auth service confirms the token is valid and tells the API who the user is.` |
| `redis.get(session)` | `The API checks the session cache to see if this user is already signed in.` |
The rule applies whether `data` is the string shorthand (`data: "..."`) or the object form's `label:` field. The `fields:` array still uses code-flavored names + types (`{ name: items, type: list[OrderItem] }`) โ that's a schema, not narration, so it stays technical.
## Before Creating Flows
If `openhop --version` fails with `command not found`, OpenHop's CLI isn't installed yet. Run `npx openhop init` yourself to install it, then continue with the steps below. (`init` copies the skill into the local AI-client config and primes the npm cache; you can keep using `npx openhop โฆ` for the rest of the session, or the user can `npm install -g openhop` for a global binary.)
Once `openhop --version` (or `npx openhop --version`) succeeds, **lock in whichever form worked** โ bare `openhop` if globally installed, otherwise `npx openhop` โ and use that exact prefix for every subsequent command in this session (`push`, `patch`, `list`, `serve`, etc.). Don't mix forms; the bare command will fail if there's no global install.
Then verify the OpenHop API server is running:
```bash
curl -s http://localhost:8787/health
```
If it returns `{"status":"ok"}`, OpenHop is ready.
If not running, start OpenHop with one command โ both the API and the web UI come from the npm package:
```bash
npx openhop demo # one-shot: starts API + web UI, posts a starter flow, opens the browser
# or
npx openhop serve # long-lived: starts API + web UI, no starter flow, no browser
```
Once the health check returns `{"status":"ok"}`, push a flow with `openhop push <file.yaml> --json` and use the `url` field from the response โ never tell the user to open the bare `http://localhost:8788` (that's the flow-list page, not a render).
## How to Work: Sketch โ Detail โ Polish
### Phase 1: SKETCH (always start here)
Write a YAML file with just nodes and steps. No colors, icons, fields, or sub-flows.
```yaml
meta:
title: "Order Processing"
path: my-app/backend
flow:
nodes:
- id: user
label: User
- id: api
label: POST /orders
- id: db
label: Database
steps:
- from: user
to: api
data: HTTP Request
- from: api
to: db
data: Save order
- from: db
to: api
data: Order ID
- from: api
to: user
data: Response
```
Push it:
```bash
openhop push flow.yaml --json
```
Output:
```json
{ "id": "abc123", "title": "Order Processing", "url": "http://localhost:8788/flow/abc123" }
```
Parse the response and return the `url` field to the user.
### Phase 2: DETAIL (iterate with PATCH)
Write a patch YAML file to add detail:
```yaml
# patch.yaml
operations:
- op: update-nodes
nodes:
- id: db
type: database
icon: "logos:postgresql"
color: "#336791"
- op: rename-nodes
nodes:
- id: api
label: Order Service
```
Apply it:
```bash
openhop patch abc123 patch.yaml
```
### Phase 3: POLISH (add data fields, sub-flows, diff highlighting)
```yaml
# polish-patch.yaml
operations:
- op: update-step
index: 1
step:
from: api
to: db
data:
label: INSERT order
fields:
- name: items
type: "list[OrderItem]"
- name: total
type: float
added: true
- op: set-flows
nodes:
- id: api
flow:
nodes:
- id: validate
label: Validate
- id: save
label: Save to DB
steps:
- from: validate
to: save
data: validated order
```
```bash
openhop patch abc123 polish-patch.yaml
```
## CLI Commands
```bash
openhop serve # Start API + web UI (:8787 + :8788)
openhop validate <file.yaml> # Local schema check, no server needed
openhop validate - # Validate from stdin
openhop push <file.yaml> --json # Push a flow โ parse `url` from JSON response
openhop push - --json # Push from stdin (pipe YAML)
openhop get <flow-id> # Fetch a flow by id (full JSON)
openhop list # List all flows
openhop patch <flow-id> <patch.yaml> # Apply patch operations
openhop patch <flow-id> - # Patch from stdin
openhop remove <flow-id> # Delete a flow
openhop help --json # Full machine-readable command tree
```
Every command supports `--json` for machine-readable output. Use it whenever you'll parse the result. Exit codes are semantic: `0` success, `2` usage, `3` validation, `4` not-found, `5` conflict, `6` network. **Always `validate` before `push`** when iterating โ it skips the server round-trip.
Stdin is useful when generating YAML programmatically:
```bash
echo 'meta:
title: Quick Test
flow:
nodes:
- {id: a, label: A}
- {id: b, label: B}
steps:
- {from: a, to: b, data: test}' | openhop push - --json
```
## Schema Reference
### Root
- `meta` (required): { title (required), description, path }
- `flow` (required): { nodes (required, min 1), steps }
### Node
- `id` (required): alphanumeric + hyphens + underscores
- `label` (required): display name โ short noun phrase, **โค 4 words** so it fits the fixed-width label slot (`"Stripe"`, `"Order Service"`, `"Auth API"`). Longer labels truncate with "โฆ".
- `type`: **closed enum, exactly one of**: `actor | endpoint | auth | database | external | cache | queue | service | docker | k8s | scheduler | ai_agent | browser | custom`. Anything else fails validation. Default if omitted: `service`.
- `icon`: Iconify icon ID (e.g. `"logos:postgresql"`) โ overlays on top of the node's pixel art. Works on any `type`, not just `custom`. See the **Icons** section below for which icon sets render correctly on the dark canvas.
- `color`: hex color
- `flow`: nested sub-flow `{ nodes, steps }` โ makes the node expandable; the renderer shows a "+" badge and clicking zooms into the inner flow. Infinite depth supported (sub-flows can themselves contain nodes with sub-flows). Pair with `drilldown: true` on a step targeting the parent node to auto-open the sub-flow on playback. See [`examples/sub-flows.yaml`](../../examples/sub-flows.yaml).
> **Critical: types are categories, labels are names.** The 14 `type` values are how the renderer knows which sprite + color to draw (database = barrel, cache = lightning, etc.). The `label` is what the user reads on the node. **Never** put a variant name (like `redis`, `oauth`, `stripe`) into `type` โ that's a label. Put it in `label`, and use the matching category in `type` (`cache`, `auth`, `external`).
>
> **When nothing fits**, use `type: custom` and set your own `icon` + `color`. Don't invent new type values โ the schema is closed.
### Node Type Variants (pick the right type, then a concrete instance)
Each node type has common real-world variants. Use them to choose an accurate `label` and, where applicable, a matching Iconify icon. First entry is the canonical/most common variant for that type.
| Type | Common variants (use as `label`, NOT `type`) |
| --------- | ------------------------------------------------------------------------------------------------------------ |
| actor | user, admin, customer, operator, agent, bot, service-account, system |
| endpoint | rest-api, graphql, grpc, webhook, websocket, sse, rpc |
| auth | oauth, jwt, session, api-key, saml, ldap, mfa |
| database | postgres, mysql, mongodb, sqlite, cassandra, dynamodb, cockroachdb, bigquery, snowflake, elasticsearch, disk |
| external | stripe, twilio, sendgrid, github, slack, openai, anthropic, firebase, s3, maps-api |
| cache | redis, memcached, ram, cdn, http-cache, local-cache |
| queue | kafka, rabbitmq, sqs, pubsub, nats, kinesis, celery |
| service | microservice, worker, processor, orchestrator, gateway, proxy, loadbalancer |
| docker | container, sidecar, init-container, compose-service |
| k8s | pod, deployment, statefulset, daemonset, job, cronjob, service, ingress |
| scheduler | cron, airflow, temporal, celery-beat, sidekiq, bullmq |
| ai_agent | llm-agent, chatbot, copilot, research-agent, coding-agent, browsing-agent, mcp-agent |
| browser | chrome, firefox, safari, edge, headless-browser, playwright, puppeteer, webview |
| custom | (anything โ also set `icon` and `color`) |
### Step
Either a move step, parallel, create, or destroy:
- Move: `{ from, to (string or string[]), data (string or object), drilldown (bool) }`
- Parallel: `{ parallel: [move steps] }` (min 2). All sub-steps fire **concurrently** on playback โ pixels travel at the same time. Use this when two or more transfers logically happen together, e.g. an orchestrator fans out work to several services at once, or two upstream nodes deliver payloads to the same target in the same tick. Each sub-step is itself a move (`from`/`to`/`data`). See [`examples/parallel.yaml`](../../examples/parallel.yaml) for an isolated demo and [`examples/self-loops.yaml`](../../examples/self-loops.yaml) / [`examples/order-flow.yaml`](../../examples/order-flow.yaml) for in-context use.
```yaml
- parallel:
- from: api
to: order-service
data: { label: order payload, fields: [{ name: items, type: list }] }
- from: authz
to: order-service
data: { label: auth context, fields: [{ name: user_id, type: int }] }
```
- Create: `{ create: "node-id", from: "creator-node", node: { id, label, type?, icon?, color? }, data? }`. Use when a node is **brought into existence** by another node mid-flow โ a dispatcher spawning a worker, a request handler instantiating a session object, a service allocating a temporary buffer. The node **does NOT need to be listed in the top-level `flow.nodes`**; `create:` adds it to the canvas at this step's tick, animating a spawn pixel from `from` into the new node. Subsequent steps may reference the new id like any other node.
```yaml
- create: worker
from: dispatcher
node:
id: worker
label: Worker
type: service
data:
label: spawn
fields:
- name: job_id
type: string
```
- Destroy: `{ destroy: "node-id" }`. Use when a node should **disappear from the canvas** after this step โ the ephemeral counterpart of `create:`. Common pairings: a worker reporting result then terminating, a session lifecycle ending, a temporary buffer being released. The destroyed node fades out; any subsequent step that references it is invalid.
```yaml
- destroy: worker
```
Pair `create:` and `destroy:` to model **lifecycle**: a node exists only between its create step and its destroy step. See [`examples/create-destroy.yaml`](../../examples/create-destroy.yaml) for a complete spawn-work-terminate flow.
### Data
Either a string (sketch) or object (detailed):
**String** โ just a label:
```yaml
data: "HTTP Request"
```
**Object** โ with optional fields:
```yaml
data:
label: "Order payload" # required
color: "#4aff7a" # optional โ override pixel color (hex)
fields: # optional โ shown in tooltip on hover
- name: items # required
type: "list[OrderItem]" # optional
- name: total
type: float
added: true # optional โ green highlight (new field)
- name: old_field
removed: true # optional โ red strikethrough
- name: amount
changed: true # optional โ yellow highlight (modified)
```
**Array** โ multiple data objects sent simultaneously:
```yaml
data:
- label: request body
fields:
- name: items
type: "list[Item]"
- label: auth context
fields:
- name: user_id
type: int
```
## PATCH Operations
All operations support multiple items. Apply with `openhop patch <id> <file.yaml>`.
| Operation | Fields | Description |
| ------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------- |
| add-nodes | nodes: [{id, label, type?, icon?, color?}] | Add nodes |
| remove-nodes | nodes: ["id1", "id2"] | Remove nodes + their steps |
| rename-nodes | nodes: [{id, label}] | Change labels |
| update-nodes | nodes: [{id, type?, icon?, color?}] | Update properties |
| set-flows | nodes: [{id, flow: {nodes, steps}}] | Add sub-flows |
| clear-flows | nodes: ["id1"] | Remove sub-flows |
| add-steps | index?: N, steps: [...] | Insert steps at 0-based `index` (same semantics as `Array.splice`). Omit `index` to append. |
| remove-steps | indices: [0, 3] | Remove steps by index |
| update-step | index: N, step: {...} | Replace a step |
## Icons
The canvas is dark โ use light-theme icons.
Pick from either:
- `simple-icons:<brand>` โ covers ~3000 brand logos and auto-recolors to white.
- `icon-sets.iconify.design/?palette=colorful` โ sets with baked-in bright colors (e.g. `logos:google-icon`, `logos:postgresql`, `twemoji:*`).
Avoid `logos:openai-icon`, `logos:vercel-icon`, `logos:anthropic-icon`, and similar bare-path "icon" variants โ they render black. Use `simple-icons:openai`, `simple-icons:vercel`, `logos:claude-icon` instead.
## Tips
- Start with 3-5 nodes. Add more only when needed.
- Use string data for sketch, object data for detail.
- Broadcast: `to: [db, cache]` sends to multiple targets in one step.
- Parallel: `parallel: [{from: a, to: b}, {from: c, to: d}]` for concurrent movements.
- `drilldown: true` on a step auto-zooms into the target's sub-flow during playback.
- Use `meta.path` to organize flows in folders (e.g. "my-app/backend").
- Iterate: push a sketch first, then refine with patch operations. Don't try to get everything right in one push.
- Both push and patch validate locally before sending to the server.
don't have the plugin yet? install it then click "run inline in claude" again.