{"slug": "langgraph-vs-vincent-generic-graph-primitives-against-an-opinionated-coding", "title": "LangGraph vs vincent: generic graph primitives against an opinionated coding-agent runtime", "summary": "Vincent, a coding-agent runtime built by developer lezli01, is positioned as an opinionated alternative to LangChain's LangGraph, which the author characterizes as \"totally generic\" in handing over nodes, edges, state, and persistence without taking a position on what the graph means. Both systems share the same core primitives — nodes versus steps, checkpointers versus a daemon-owned SQLite task snapshot, interrupts versus blocked tasks — but Vincent adds repository-specific controls including per-lane worktrees and branches, `needs:` dependencies, `retry_backoff`, timeouts, and human sign-off gates. The author argues the overlap is real but the substance differs: LangGraph moves typed application state between functions, while Vincent moves work performed against a repository.", "body_md": "[Vincent](https://github.com/lezli01/vincent) executes a graph of steps, persists the state, retries what fails, runs branches concurrently, and stops for a human. So does [LangGraph](https://docs.langchain.com/oss/python/langgraph/overview). I build vincent, so “have I rebuilt a worse version of something that already exists” is a question I would rather answer properly than wave off.\n\nThe answer is no, and the reason is more interesting than “different scope”. LangGraph is **totally generic**. It hands you nodes, edges, state, and persistence, and takes no position whatsoever on what the graph means. That is the design, and it is a real strength. Vincent is the opposite kind of artifact: it is an attempt to answer how agentic development should be regulated and helped. What a coding agent is allowed to touch. How its work is isolated. What counts as it having actually worked. Where a person has to sign off. How several agents’ work comes back together without them overwriting each other.\n\nThat position is the whole product. Everything below is an attempt to say what it buys and what it costs.\n\n## Where they genuinely overlap\n\nThe overlap is not superficial, and pretending otherwise would be dishonest. Both treat agentic work as something structured rather than one process invocation, and both arrive at broadly the same primitives:\n\n| Concept | LangGraph | vincent | \n|---|---|---|\n| Unit of execution | node | step | \n| Deterministic work | a function | `type: command` | \n| Agentic work | an LLM or agent node | `type: agent` | \n| Human interaction | interrupt | `type: manual` , a`blocked` task | \n| Conditional execution | conditional edge | `if:` ,`type: condition` | \n| Repetition | graph cycle | `type: loop` ,`type: break` | \n| Composition | subgraph | `type: include` | \n| Concurrency | concurrent nodes | `type: parallel` | \n| Independent parallel jobs | graph branches | `type: fan_out` child tasks | \n| Dependencies | edges | lane `needs:` | \n| Durable state | checkpointer | the daemon-owned task snapshot | \n\nThe right-hand column is a real graph, not a linear list dressed up. `type: parallel` runs sub-steps concurrently inside the task’s single worktree. `type: fan_out` is the other thing entirely: each lane becomes a real child task with its own worktree and branch, and those branches get merged back. Once lanes name each other through `needs:`, that lane set is a DAG and the step [runs it in topological rounds](https://blog.lezli01.is-a.dev/blog/vincent-dag-workflows/), merging what is finished before spawning what those merges made eligible. Side by side, the shapes are similar and the substance is not:\n\nBoth systems also converged on the same durability argument, which is that a workflow is persisted execution rather than the lifetime of one process. LangGraph does it with checkpointers and resumable runs. Vincent does it with a daemon that owns SQLite, decides when steps run, applies `retry_backoff`, enforces timeouts, records attempts, and parks a task in `blocked` when a person is needed. Different implementations, same observation underneath.\n\n## What each one is actually moving\n\nLangGraph moves structured application state between functions. The node is application code, and the thing that travels is a typed structure the nodes read and return updates to:\n\n``` python\nfrom typing import TypedDict\n\nclass State(TypedDict):\n    requirement: str\n    plan: list[str]\n    code: str\n    attempts: int\n\ndef retrieve(state: State) -> State: ...\ndef generate(state: State) -> State: ...\ndef evaluate(state: State) -> State: ...\n```\n\nVincent moves work performed against a repository. Its unit is a YAML step, and the shape most work takes is an agent that writes code, a check the agent cannot talk its way past, a gate, and a publish step that only runs once a person approved. This is `examples/feature-pr.yaml` from vincent’s own repo, with the prompt bodies and the inline comments cut:\n\n```\n  - id: implement\n    type: agent\n    prompt: |\n      Implement the following task in this repository.\n    check: go build ./... && go test ./...\n    check_timeout: 15m\n\n  - id: commit\n    type: command\n    run: 'git add -A && git commit -m \"{{.Task.Title}}\"'\n\n  - id: review\n    type: manual\n    instructions: |\n      Review the diff for task #{{.Task.ID}} on branch {{.Task.BranchName}}.\n```\n\nIn the Python, the node’s return value is the state transition. In the YAML, the step’s return value is almost incidental. What it left on disk is the point.\n\n## The repository is the workflow state\n\nThis is the load-bearing difference, so let me be exact about it. A vincent agent step does not hand back something like:\n\n```\n{\n  \"modified_files\": [\"src/api.ts\", \"test/api.test.ts\"]\n}\n```\n\nThe files have been modified. **The repository itself is part of the workflow state**, so the worktree is the return value, and everything downstream reads it directly rather than reading a report about it.\n\nThat one decision is what makes the rest of vincent’s vocabulary possible. `check:` is not a validator over a dict; it runs after the step body in that worktree, with a command step’s environment, and non-zero fails the attempt. The diff a human reviews at a `manual` gate is `git diff` against the base branch, not a serialized summary. A fan-out join is an actual git merge, so a conflict is conflict markers in files on disk, which is why `merge.on_conflict` has to choose between `block` (stop, leave the worktree conflicted, let a person resolve it in place) and `agent` (hand the conflicted paths to an agent step that has its own check). None of that has a representation in a state dict, because a state dict has no filesystem under it.\n\nThat is what a `fan_out` step looks like drawn at the level git sees:\n\n## The agent step is a black box\n\nVincent’s `type: agent` is deliberately opaque. It renders a prompt, hands it to an adapter for Claude Code, Codex, or Cursor, and waits. What the agent does inside, its planning, its searching, its tool calls, its edits, is not vincent’s business and vincent has no opinion about it.\n\nThat is the cleanest architectural boundary between the two systems, because LangGraph is a good tool for building what happens inside that box:\n\nSo the two systems are not competing for the same job. One owns the control flow inside an AI application. The other owns the operational execution of that application’s output across a real codebase.\n\n## The vocabulary is the regulation\n\nHere is what a running vincent task looks like from the outside, which is the best single piece of evidence I have for the argument:\n\nEvery box on that screen is a domain noun a generic graph runtime does not have. LangGraph’s nouns are state, messages, nodes, edges, and checkpoints, all content-free on purpose. Vincent’s are repository, task, worktree, branch, diff, command, coding agent, check, child task, merge, conflict, and human gate. Each of those is a position taken, and taken together they are the regulation:\n\n- **`check`:** an agent reporting success is not evidence. The check runs after the step body, in the worktree, and its failure is appended to the next attempt’s prompt automatically, so a failed check becomes the agent’s next instruction rather than a message to you.\n- **`manual`:** a person is a scheduled participant, not an interruption. The task enters`awaiting_gate` and releases its concurrency slot while it waits, and rejecting moves it to`blocked` .\n- **`max_retries`, `retry_backoff`, `timeout`:** all three are per-step fields with workflow-level defaults, which is the`max_retries: 1  retry_backoff: 30s  timeout: 3m0s` reading off the status bar at the bottom of that screenshot.\n- **`blocked`:** a resting state rather than a dead end. Work stops where it broke, with its worktree intact, and waits for a retry, an edited prompt, a skip, or an ad-hoc repair agent in the same worktree.\n- **`fan_out`:** parallel agents get isolated branches, not a shared mutable state object.`max_depth` is 3 and`max_tasks` is 64, both checked when the task is created, so an oversized or cyclic plan is a 400 naming what is wrong instead of two hundred worktrees you find later.\n\nYou can build every one of those on generic graph primitives. Nothing in the list is beyond a Python function and a conditional edge, and I want to be clear that this is not a capability claim. The difference isn’t what is possible. It’s what is **default**. In a generic runtime a guardrail is something you remember to build; here it is a field you would have to deliberately remove, and a step type you would have to deliberately not use. That is a different claim from “vincent can do more”, and it is the only one I am making.\n\nLangGraph orchestrates what an AI agent does. Vincent orchestrates what AI coding agents do to a software project.\n\n## The operational problem, not the model problem\n\nGenerating a plausible diff is close to free now, and it keeps getting cheaper. Knowing whether that diff is correct, knowing exactly what it touched, and having a person who chose to let it through are the expensive parts, and none of them get cheaper when the next model lands. That is the half I care about, because it is the half a better model does not solve.\n\nOne agent you can watch. Four, you cannot, and at that point the question stops being whether it is a good agent and becomes an operational one: what is each of them allowed to touch, how do you tell afterwards what each of them actually did, and what happens when two of them edit the same file. Vincent’s answers are worktree isolation, a branch per lane, and a real merge at the join. The useful property of those three is that they hold whether or not the agent behaved.\n\nRegulation, in the sense I mean here, is constraint that survives the agent being wrong. A `check:` is not a suggestion an agent can argue with; it is a command, and its exit code decides the attempt. A `manual` gate is not advice; the task does not advance until a person says so. `max_depth: 3` and `max_tasks: 64` are a ceiling on how far a fan-out can spread, checked when the task is created rather than discovered when the worktrees appear. Drawn as a path, one agent step and everything that can stop it looks like this:\n\nThe second output is a trail. `GET /v1/tasks/{id}/steps` returns every step run and every attempt carrying the rendered prompt, the rendered check, the exit code, the duration, and which level supplied the agent and the model, next to the per-attempt transcript and the diff sitting on a branch. A step a person skipped is even distinguishable from one an `if:` guard skipped, because the guard-skipped row carries `skip_reason: \"condition\"` and the hand-skipped one carries nothing. That record exists because the work happened in git and in a database rather than in a variable.\n\nThe gate is the piece I would defend hardest, and its credibility rests on something small: a task at `awaiting_gate` releases its concurrency slot. A waiting human is not a lock the rest of the board queues behind, so other tasks keep running while one sits waiting for a decision. That is what turns “a person signs off” into a schedulable event instead of a good intention.\n\nNone of this makes an agent’s output correct, and I would rather be blunt about that, because a claim that sounds like safety is cheap to make. A check asserts what somebody wrote it to assert and nothing more. A gate is worth exactly as much as the attention of the person reading the diff. Isolation between agents is not isolation from the machine they run on. What the vocabulary buys is narrower: the work becomes **reviewable**, and the damage a wrong answer can do stays bounded by a branch nobody has merged yet.\n\nMy read is that every team running more than one agent against a repository ends up answering these questions, deliberately or by accident. Vincent is one deliberate answer, and the reason I would rather ship it than argue it is that you can run it against your own repository and find out whether the answer holds.\n\n## What being opinionated costs\n\nThe strongest version of the objection is straightforward, and it is correct: a determined team can implement all of vincent’s semantics on top of a generic graph runtime, and what they get in exchange is a graph they can reshape however they like. Vincent’s opinions are a floor and a ceiling at the same time.\n\nThe ceiling is real and I would rather name it than let someone discover it. Vincent has nine step types. Its control flow is structured, not free: steps run in order, top to bottom, guarded by `if:`, ended early by `type: condition`, repeated by `type: loop`, left by `type: break`, and spliced from another file by `type: include`. There is no arbitrary edge from any step to any other step. If your problem needs a shape that structured control flow cannot draw, there is no syntax for it and no escape hatch that gives you one.\n\nThe scope is a harder limit than the syntax. Vincent only fits work that is SDLC-shaped. Its entire vocabulary assumes a git repository underneath, so if the thing being orchestrated is not a change to a codebase, every one of its concepts is dead weight and you should use something generic. And everything inside the agent is delegated by design. Message state, model and tool routing, retrieval, reasoning loops, long-term memory: vincent has none of it, and that is absence by design rather than weakness.\n\nOne more cost, which I have [named before](https://blog.lezli01.is-a.dev/blog/vincent-0-7-release/): vincent is pre-1.0, so the workflow and API surfaces can still move.\n\n## They compose\n\nBecause the boundary is clean, the two stack rather than compete. A vincent `type: command` step can invoke a LangGraph-based analysis tool and let a `check:` decide whether the run counted. A coding agent behind a vincent adapter can be a LangGraph application internally, and vincent will neither know nor care, because the contract at that boundary is a prompt in and a changed worktree out.\n\nNot every coding agent needs LangGraph, and not every LangGraph application needs vincent. But an organization that builds a sophisticated custom agent and then wants to run it repeatedly, concurrently, and under human control against real repositories has two separate problems, and they happen to have two separate answers.\n\n## Where I have landed on positioning\n\nVincent should not be positioned as an alternative to LangGraph. That puts it in the wrong category and invites a comparison on features neither one is trying to win. If the hard part of your problem is inside the AI application, in the state, the routing, the retrieval, or the reasoning loop, reach for a generic graph runtime and enjoy the fact that it takes no position on your domain. If the hard part is what a fleet of coding agents is allowed to do to a repository, and how you would know afterwards that it worked, then a graph runtime gives you the primitives and leaves the entire question open.\n\nThat is the trade vincent makes: a vocabulary narrow enough to be useless outside software development, in exchange for guardrails you get by default instead of by discipline. The [documentation](https://lezli01.is-a.dev/vincent) and the [repository](https://github.com/lezli01/vincent) have the detail, and the rest of what I work on is at [lezli01.is-a.dev](https://lezli01.is-a.dev).", "url": "https://wpnews.pro/news/langgraph-vs-vincent-generic-graph-primitives-against-an-opinionated-coding", "canonical_source": "https://blog.lezli01.is-a.dev/blog/langgraph-vs-vincent/", "published_at": "2026-09-09 00:00:00+00:00", "updated_at": "2026-09-15 09:37:53.231553+00:00", "lang": "en", "topics": ["ai-agents", "ai-tools", "developer-tools", "ai-products"], "entities": ["Vincent", "LangGraph", "LangChain", "lezli01", "SQLite"], "alternates": {"html": "https://wpnews.pro/news/langgraph-vs-vincent-generic-graph-primitives-against-an-opinionated-coding", "markdown": "https://wpnews.pro/news/langgraph-vs-vincent-generic-graph-primitives-against-an-opinionated-coding.md", "text": "https://wpnews.pro/news/langgraph-vs-vincent-generic-graph-primitives-against-an-opinionated-coding.txt", "jsonld": "https://wpnews.pro/news/langgraph-vs-vincent-generic-graph-primitives-against-an-opinionated-coding.jsonld"}}