| name | run-langgraph-conversation |
| description | Runs a user-specified conversation against the local LangGraph dev server, captures the resulting thread_id, and hands off to the follow-langgraph-conversation skill for full trace analysis. Use when the user wants to execute a test conversation (single or multi-turn) against a LangGraph.js graph and investigate agent behaviour in one shot. |
Execute a user-described conversation against a local LangGraph.js dev server, then delegate the captured `thread_id` to the `follow-langgraph-conversation` skill for state + LLM-trace analysis.
Input (from the user invocation): a natural-language description of what to ask the agent. May be single-turn or multi-turn, and may include optional input-state fields the graph reads (e.g. a user id, a tenant/customer key, a role) — discover what those fields are from the graph rather than assuming.
This skill talks to the **LangGraph.js dev server** over HTTP (the server `npx @langchain/langgraph-cli dev` / `langgraph dev` exposes, default port `2024`). It is graph-agnostic: it discovers the registered graph and passes whatever input fields the user supplies. Nothing here is specific to any one project.
Several things vary per project and must be discovered, not assumed — read them from the project's own files (Phase 0):
- The port — read it from the dev script / compose mapping / env; treat
2024 as a last-resort default, and ask the user if it's unclear.
- The graph id (
assistant_id) — discover it from /assistants/search; do not hardcode a name.
- How the server is started —
npm run dev, langgraph dev, a docker compose service, etc. Probe first; only suggest a start command if it's down.
<quick_start>
- Discover the server config (port, start command, graph) by scanning the project's files.
- Ensure the dev server is up on the discovered port (probe
/info; start it only if down).
- Discover the registered graph id via
/assistants/search.
- Create a fresh thread via
POST /threads.
- For each turn the user specified, call
POST /threads/{id}/runs/wait with messages + any input-state fields.
- Echo the turn-by-turn exchange back to the user along with the
thread_id.
- Invoke the
follow-langgraph-conversation skill with that thread_id.
</quick_start>
**Phase 0: Discover the server configuration**
Don't assume port 2024 or how the server starts — read it from the project first. Scan, in order, and stop once you have a port + a start command:
langgraph.json (repo root) — confirms a LangGraph.js project. Its graphs map lists the registered graph ids (candidates for assistant_id); its env key names the env file (commonly .env) the dev server loads.
package.json scripts — the dev script is the real start command and often pins the port, e.g. "dev": "langgraph dev --port 8123" or "@langchain/langgraph-cli dev --port …". No --port flag → the CLI default is 2024.
docker-compose.y*ml — if the server runs in a container, the published host port is the ports: mapping (host:container) for the langgraph service, and the start command is docker compose up -d <service>.
.env / .env.* (including the file langgraph.json's env points at) — a PORT / LANGGRAPH_PORT var if the project wires one.
- README — for a documented port or start command.
Resolve the port from the strongest signal (dev-script --port > compose mapping > explicit env var > default 2024). If sources conflict, or none is found and /info isn't reachable, ask the user for the port. Carry the resolved port through every URL below (this doc writes 2024 as a placeholder).
**Phase 1: Ensure the dev server is running and find the graph**
Probe on the port resolved in Phase 0:
curl -s -o /dev/null -w "%{http_code}" http://localhost:2024/info
200 → continue. (The body looks like {"flags":{"assistants":true,"crons":false}}.)
404 / connection refused → before starting anything, sweep for an already-running instance on another port (the server may be up somewhere other than the configured port):
for p in $(lsof -nP -iTCP -sTCP:LISTEN 2>/dev/null | grep -oE ':[0-9]+ \(LISTEN\)' | grep -oE '[0-9]+' | sort -un); do
body=$(curl -s -m 1 "http://localhost:$p/info" 2>/dev/null)
case "$body" in *'"flags"'*) echo "LangGraph instance on port $p";; esac
done
If instances exist, confirm which one serves THIS project before using it (host process: ps -p <pid> -o command= + lsof -p <pid> | grep cwd; container: docker ps + docker inspect; or match /assistants/search graph ids against this project's langgraph.json) — running the conversation against another project's server produces a thread the analysis can't explain. Only if no instance serves this project: start it with the project's dev command — commonly npm run dev, npx @langchain/langgraph-cli dev, or a docker compose up -d service if the graph runs in a container. Then poll /info until 200 (typical ready time 5–15s; fail after ~60s with a clear message). Don't guess a start command if the repo documents one — read package.json scripts / README / langgraph.json first.
Discover the graph id (don't assume it's "agent"):
curl -s -X POST http://localhost:2024/assistants/search \
-H "Content-Type: application/json" -d '{"limit": 10}' | python3 -c "
import json, sys
for a in json.load(sys.stdin):
print(f' assistant_id={a.get(\"assistant_id\")} graph_id={a.get(\"graph_id\")}')
"
Use the returned graph_id as the assistant_id in Phase 3. If several graphs are registered, pick the one the user named, or ask.
**Phase 2: Create a new thread**
curl -s -X POST http://localhost:2024/threads \
-H "Content-Type: application/json" -d '{}'
Extract thread_id from the JSON response. Reuse it for every turn below and for the handoff in Phase 4.
**Phase 3: Run each turn synchronously**
For every turn the user described:
curl -s -X POST "http://localhost:2024/threads/{THREAD_ID}/runs/wait" \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "{GRAPH_ID}",
"input": {
"messages": [{"role": "user", "content": "{USER_TEXT}"}]
}
}'
Rules:
messages is the one field every graph takes. Add other input-state fields only when (a) the user supplied them and (b) the graph actually declares them. Discover the graph's input fields from its state definition (e.g. src/**/state.ts, an InputState annotation) or by inspecting a prior thread's values. Unknown keys are silently ignored by LangGraph, so a wrong guess fails silently — verify.
- Match each field's declared type exactly. A field typed as a string array must be passed as an array (
["x"], not "x"); a custom reducer can coerce or drop a mistyped value unpredictably. This is the most common input mistake.
/runs/wait blocks until the turn completes. Chain turns sequentially using the same thread_id so the agent sees prior context.
- From each response, extract the last AI message (
messages[-1].content) and show it to the user with the turn index.
- If the graph exposes top-level decision signals on the response (routing/classification/branch fields, a detected tool/handoff, a confidence score, etc.), surface those too so the user can see which path the turn took. Which fields exist is graph-specific — read them from the response
values, don't assume a fixed set.
- Handoff turns (channel contract). Check the final AI message's
additional_kwargs: is_handoff: true means the agent signalled a transfer — print handoff_type + handoff_reason (+ handoff_metadata.success_message) with the turn. A delegated_to field WITHOUT is_handoff means the agent delegated the turn and kept the conversation. Also surface handoff state slots if the graph writes them (e.g. pendingHandoff, handoff).
- No middleware locally. In production a fronting middleware routes on handoff signals (re-sends
off_topic turns to another agent; flips routing after completed/abandon). The dev server does none of that — turns after a handback keep hitting the same agent, which simulates "the middleware never transferred". Treat a handback as the realistic end of the test conversation unless the user explicitly wants to probe post-handback behavior.
- Identity sticks from turn 1. Channel-fronted graphs commonly declare session-context fields (
user_id, , , ) with preserve-initial reducers: the first non-null value wins and later turns CANNOT change it. Pass identity on the first turn; to test a different identity, start a new thread — don't try to switch mid-thread (it silently does nothing).
After all turns finish, print:
thread_id: {uuid}
- 1 line per turn:
[N] user: "..." → agent: "..." (truncated, with any non-default decision signals).
**Phase 3b (opt-in): Streaming wire capture**
ONLY when the user asks to verify streaming/wire behavior (token order, handoff events, custom control-plane events) — the default for turn-running stays /runs/wait. Replace the turn's call with a raw SSE capture:
curl -sN -X POST "http://localhost:2024/threads/{THREAD_ID}/runs/stream" \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "{GRAPH_ID}",
"input": { "messages": [{"role": "user", "content": "{USER_TEXT}"}] },
"stream_mode": ["messages-tuple", "updates", "custom"]
}' | tee /tmp/capture-turn-{N}.sse | grep "^event:" | uniq -c
messages events carry LLM tokens; updates carry per-node state deltas (where a post-model hook's or a resolver node's is_handoff final message surfaces — it may NEVER appear in the token stream); custom carries control-plane events some graphs emit (e.g. handoff, delegated_token, handoff_complete).
- Summarize the event ORDER for the user (which event types, from which nodes, in what sequence) and keep the raw
.sse file — it doubles as a golden fixture for stream-consumer tests.
- The captured run still lands on the thread, so Phase 4's analysis covers it too.
**Phase 4: Hand off to the analysis skill**
Invoke the follow-langgraph-conversation skill via the Skill tool, passing the captured thread_id (and the port / graph id if non-default). That skill produces the full state-progression + LLM-trace report. Do NOT duplicate its work here — kick it off and relay its output.
<input_format>
Accept flexible phrasings. Parse and, when ambiguous, echo your interpretation before executing. Examples:
- Single turn: "ask the agent: show me current savings rates"
- Multi-turn: "turn 1: hi. turn 2: book me an appointment tomorrow."
- With input-state context: "as user_id=123 role=["manager"], ask: what products do you offer?" — only pass fields the graph declares.
- Other languages: pass user text through verbatim; many agents are multilingual.
</input_format>
<api_reference>
LangGraph.js dev server endpoints used here:
GET /info — health probe (returns {"flags":{...}} when up; the dev server does not expose /ok or /health).
POST /assistants/search — list registered graphs (body: {"limit": N}); read graph_id.
POST /threads — create a thread (body: {}).
POST /threads/{thread_id}/runs/wait — synchronous run (body: {"assistant_id": "<graph_id>", "input": {...}}).
POST /threads/{thread_id}/runs/stream — SSE run for the opt-in Phase 3b wire capture (body adds "stream_mode": ["messages-tuple", "updates", "custom"]).
Input shape mirrors the graph's input-state annotation:
messages[] — required (LangChain-style {role, content} entries).
- Any additional input-state fields the graph declares (optional). Discover them; don't assume.
</api_reference>
<anti_patterns>
Always probe /info first. If it's down, start it with the project's documented dev command — and if the graph runs in a container, the right command is the container one (e.g. docker compose up -d), not a host npm run dev that skips the container's startup dependencies.
Don't hardcode `assistant_id: "agent"`. Discover the registered `graph_id` from `/assistants/search` and use it. A wrong id fails the run.
Don't assume `2024`. Resolve the port from the project (Phase 0: dev script `--port`, compose port mapping, env var) and ask the user if it's unclear. Probing the wrong port reads as "server down" and sends you starting a server that's already running elsewhere.
Use `/runs/wait` for ordinary turn-running, not `/runs/stream` or `/runs` — non-blocking runs complicate turn chaining. The ONE exception is the deliberate Phase 3b streaming capture (verifying token/event order on the wire), which consumes the SSE stream to completion before the next turn.
Multi-turn conversations MUST reuse the same `thread_id`. A new thread per turn means the agent loses conversation memory and the follow-up analysis only covers the last turn.
Match each input-state field's declared type. The classic failure is passing a string where the graph declares a string array — a custom reducer then stores something other than what you intended. Arrays must be arrays.
Don't paste entire JSON responses back to the user. Extract the final AI message content plus any decision signals and summarise.
The point of this skill is analysis, not just execution. Always invoke `follow-langgraph-conversation` at the end with the captured `thread_id`. Stopping after the conversation runs defeats the purpose.
<success_criteria>
- The dev server was confirmed (or freshly started and confirmed) healthy via
/info.
- The registered graph id was discovered and used as
assistant_id.
- A new
thread_id was captured.
- Every user-specified turn ran to completion with no HTTP errors, and the final AI reply (+ any decision signals) was surfaced.
- The
follow-langgraph-conversation skill was invoked with that thread_id and its report was relayed to the user.
</success_criteria>