Coming from LangChain
You know how to build an agent. This page maps the LangChain pieces you already reach for onto their Harn equivalents so you can port a working idea rather than relearn the vocabulary.
For LangGraph specifically — Node, Edge, State, Channel, super-step,
interrupt — the vocabulary table in
Coming from elsewhere is the
cross-reference. This page covers the LangChain side and the day-to-day
mechanics.
| LangChain | Harn | Where |
|---|---|---|
LCEL a | b | c | the pipe operator, x |> f(_) |> g(_) | below |
StateGraph | workflow_graph + workflow_execute, or a portable workflow bundle | below |
@tool | an inline tool declaration, or harn tool new for a shareable one | below |
with_structured_output(Model) | output: {schema: T} on the call | below |
RunnableRetry / .with_retry() | with_retry from std/llm/handlers | below |
| LangSmith | harn portal and harn usage, local and included | below |
checkpointer | checkpoint_stage from std/checkpoint | below |
Composition
LCEL's | chains runnables left to right. Harn's pipe does the same for
ordinary functions, with one difference: the placeholder is explicit, so the
piped value can land in any argument position rather than only the first.
fn double(x: int) -> int {
return x * 2
}
fn add(x: int, y: int) -> int {
return x + y
}
fn main(harness: Harness) {
const out = 3 |> double(_) |> add(_, 1)
harness.stdio.log(to_string(out)) // 7
}
The reasoning behind the explicit _ is in
ADR-0001. Note that Harn's pipe composes plain
function calls; it is not a separate runnable protocol with its own streaming
and batching semantics. Model calls, retries, and fallbacks compose through
middleware instead.
Graphs and workflows
A StateGraph becomes a Harn workflow. There are two shapes, and which one you
want depends on who runs it.
For a graph your own program runs, build it with workflow_graph and execute it
in-process with workflow_execute. Nodes are stages, edges carry a branch
label, and retries and verification are node policy rather than hand-rolled
loops. See the workflow runtime.
For a graph a host runs — an IDE, an orchestrator — author a portable workflow
bundle instead. It is JSON, it validates to a stable graph_digest, and the
host executes it. See
Run a workflow bundle from the CLI.
The one thing that does not map cleanly is LangGraph's typed state with reducers. Harn's default state model is workflow artifacts and the transcript, not a typed dict merged per super-step. That gap and its design are described in the LangGraph table.
Tools
@tool decorates a Python function. The everyday Harn equivalent is a tool
declaration inside the program that uses it — typed parameters, a description
written for the model, and the body inline:
tool search(pattern: string) -> string {
description "Search the project"
return harness.process.exec("rg", "--", pattern).stdout ?? ""
}
When the tool should be shared across projects rather than living in one program, scaffold it as a package:
harn tool new summarize-diff --description "Summarize a git diff"
See Extend Harn for how tools relate to packages, connectors, and skills.
Structured output
with_structured_output(Model) binds a Pydantic model to a call. Harn takes a
type on the call's output option, and the same option controls provider-level
strictness, post-parse validation, and early stream abort:
type Verdict = {pass: bool, reason: string}
const result = harness.llm.call(prompt, nil, {
output: {schema: Verdict, strict: true, validation: "error", stream_abort: true},
schema_retries: 1,
})
result.data is the narrowed value. schema_retries re-asks the model when
parsing fails, which is the behavior you would otherwise write around a
LangChain output parser. Full option list in
harness.llm.call.
Retries and middleware
RunnableRetry wraps a runnable. Harn wraps the call: agent_loop accepts an
llm_caller closure that owns each turn's harness.llm.call, and the handlers
in std/llm/handlers compose around it.
import {default_llm_caller} from "std/llm/caller"
import {with_retry} from "std/llm/handlers"
const caller = with_retry(default_llm_caller(), {max_attempts: 4})
const result = agent_loop(harness, task, system, {
loop_until_done: true,
llm_caller: caller,
})
Prefer llm_caller({retry: {max_attempts: 4}}) for the blessed default stack —
it is the same composition plus typed reserved-status classification and
billed-empty re-dispatch. Fallback, shadow, logging, budget, cache, and circuit
breaker are sibling handlers you compose the same way. See
the handlers catalog.
Tracing and cost
LangSmith is a hosted service you sign up for. The Harn equivalents run on your machine and are part of the toolchain:
harn portal # observability UI over persisted runs
harn usage # spend and token rollups from the local event log
harn portal binds 127.0.0.1:4721 by default and reads run records from
.harn-runs/. harn usage aggregates provider_call_response events out of
the project's .harn/events.sqlite and rolls them up by provider, model, or a
day/week/month series — it reuses the cost the runtime already computed rather
than re-pricing anything. Neither needs an account or sends data anywhere.
See Debugging agent runs and harn usage.
Resuming after a crash
A LangGraph checkpointer persists graph state so a thread can resume. Harn's
closest everyday primitive is checkpoint_stage, which caches a stage's result
under a name and skips the work on a resumed run:
import { checkpoint_stage } from "std/checkpoint"
fn main(harness: Harness) {
const data = checkpoint_stage(harness.runtime, "fetch", { -> "raw" })
const cleaned = checkpoint_stage(harness.runtime, "clean", { -> data + "-clean" })
harness.stdio.log(cleaned)
}
The first argument is harness.runtime — the capability that owns the
checkpoint store. checkpoint_stage_keyed adds an identity so the same stage
name can be checkpointed per item, and the _retry variants add bounded
retries.
For an agent that parks and resumes rather than crashes and restarts, the
mechanism is different: see
the daemon agent tutorial for
agent_await_resumption, worker snapshots, and harn run --resume.
What has no direct equivalent
- A retriever abstraction. Harn has no
VectorStoreRetrieverinterface. Retrieval is something you write as a tool or a stage. - A document loader ecosystem. There is no
langchain-communityanalogue. - Chain serialization. LCEL chains do not serialize to a portable format;
Harn's portable unit is a workflow bundle or a signed
.harnpack, which is a different granularity.