Eight ways agents hand work to each other, with no particular task: roles, messages, and in the right-hand panel what each agent gets to see. What an agent sees decides what it can get right.
Next: the same patterns in code, on one real task: “Black running shoes, size 9, under ₹1,000”
Every excerpt is the real code in patterns/ and lab/. LangGraph’s model: one state object, nodes that read it and return updates, and edges that decide what runs next. Control flow is yours, explicitly.
LangGraph is an open-source, low-level orchestration library from the LangChain team, for Python and JavaScript. You describe your system as a graph: one shared state, nodes that read it and return updates, and edges that decide what runs next. It doesn’t write prompts or define agents for you, and it doesn’t need LangChain. This demo uses langgraph 1.x.
TypedDict (or pydantic model). Each key can have a reducer: Annotated[list, operator.add] appends, a plain key is overwritten.state → update. It can call a model, a tool, an API, or just do arithmetic. Only the keys it returns change.add_edge (always next), add_conditional_edges (a function picks), Send (start N copies with their own payload). Cycles are allowed.thread_id: resume after a crash, pause for a human with interrupt(), browse history and rewind.Around those four: subgraphs (a compiled graph is a node), streaming of updates, values and tokens, Command (update state and jump in one return), a long-term store, and prebuilt agent loops when you don’t want to draw one. LangChain also sells a hosted platform and LangSmith tracing around it; neither is required.
graph.invoke()builder.compile(checkpointer=…) checks the graph and returns a runnable. invoke() takes the input and a thread_id.Send fans out.Send(node, payload) for each item. Back to step 2.END finishes. interrupt() pauses and saves; Command(resume=…) continues. recursion_limit stops runaway loops.The idea to keep: you own the loop. Nothing runs that isn’t an edge, and a node is just a function, so the model is called only where you put a call.
Send. SqliteSaver with get_state_history.interrupt() inside any node pauses and persists; the answer can come hours later, from another process. lab/guards.py is one line.offers through operator.add.Send vs Command, threads vs store. It’s easy to misread when, and how often, a node runs.rejected list in state that you maintain.recursion_limit. lab/runner.py sets recursion_limit=24 as the last line of defence.interrupt(), resuming runs the whole node again from its first line, so anything before the interrupt happens twice unless it’s idempotent.Debugging is smoothest with LangSmith, LangChain’s paid tracing; without it you add your own (this project writes its own run log). The trade: you get control and durability, and you pay in code and concepts.
| You see… | Because… | Fix |
|---|---|---|
| A node never runs | No edge leads to it | Every node needs an incoming edge. Draw it: graph.get_graph().draw_mermaid() |
InvalidUpdateError at a fan-in | Two parallel nodes wrote the same plain key | Give the key a reducer: Annotated[list, operator.add] |
GraphRecursionError | A cycle with no exit, or a limit set too low | Count turns in state and route to END; set recursion_limit in the config |
| An email sent twice after approval | Resume reruns the node that called interrupt() | Do side effects after the interrupt, in their own node, or make them idempotent |
| The run forgot everything | No checkpointer, or a new thread_id | compile(checkpointer=…) and reuse {"configurable": {"thread_id": …}} |
| A change to state didn’t stick | The node mutated state instead of returning an update | Return a dict with only the keys you changed |
In this project: patterns/ has the four graphs from the live demo; lab/ has the guardrails (owner, budget, approval) with crash, resume and rewind on SqliteSaver.
The same four patterns, built in crews/. CrewAI’s model: agents with roles, tasks with expected outputs, a crew that runs them, and Flows for events. You describe the work; CrewAI runs the loop.
CrewAI is an open-source Python framework for multi-agent systems. You describe who does the work (agents with a role, goal and backstory) and what needs doing (tasks with an expected output); CrewAI writes the prompts, runs each agent’s think → act loop, and passes results between them. It is its own framework, not built on LangChain. This demo uses crewai==1.15.28.
llm and optional tools. That text becomes the agent’s system prompt.expected_output, the agent that does it. Optionally typed (output_pydantic), checked (guardrail) and run in parallel (async_execution).sequential (in order, each output is the next one’s context) or hierarchical (a manager agent delegates).@start, @listen, @router, sharing a pydantic state. Flows call crews where judgment is needed.Around those four: tools (any function, or the crewai-tools package), memory and knowledge (retrieval over past runs and your documents), delegation between agents, checkpoints, and a CLI that scaffolds projects with agents.yaml and tasks.yaml.
crew.kickoff(){query} in agent and task text is replaced by kickoff(inputs=…).context.max_iter.output_pydantic; then the guardrail. If it fails, the same agent runs again with the reason.TaskOutput, the context for later tasks. The last one is the CrewOutput.The key difference from LangGraph: the loop is inside the framework. You configure it; you don’t draw it. That’s why each step in these crews is a model call: the unit of work is an agent turn.
guardrail= plus guardrail_max_retries gives you retry-with-feedback. In LangGraph that’s two conditional edges and a counter in state. output_pydantic validates every task’s answer before the next agent sees it. A bad answer is caught at the boundary, not three steps later.agents.yaml and tasks.yaml. The design conversation is “who should check the seller?”, not “which edge?”.interrupt() and time travel. Human input is mostly human_input=True or a Flow decorator.OTEL_SDK_DISABLED and CREWAI_TELEMETRY_OPT_OUT in crews/__init__.py.LangGraph has its own costs: more code, you design every edge and state key, and a steeper start. The trade is convenience and conventions (CrewAI) against control and durability (LangGraph).
| If you need… | Pick | Why |
|---|---|---|
| A working prototype this week, the work maps to roles (research, review, write) | CrewAI | Agents and tasks in English, typed outputs and retries for free |
| Exact control of every branch, loop and join | LangGraph | You draw the graph; nothing runs that isn’t an edge |
| Cheap, fast steps mixed with a few model calls | LangGraph | Nodes can be plain code; only the judgment calls hit the model |
| Long-running runs, crash recovery, a human approving mid-run | LangGraph | Checkpoint every step, interrupt(), resume and rewind |
| A critic that sends work back with feedback | Either | One argument in CrewAI; two edges in LangGraph |
| Event-driven: something in the world starts the run | Either | CrewAI Flow (@start/@router) or your code calling graph.invoke() |
| Non-engineers editing who does what | CrewAI | agents.yaml and tasks.yaml read like a job description |
You don’t have to choose one. A crew can be a single LangGraph node, and a CrewAI Flow can call any Python, including a compiled graph. A common shape: LangGraph (or a Flow) for the skeleton, a crew where a step genuinely needs a team.
patterns/) or CrewAI (crews/): the same four patterns.