Skip to main content
When you build an agent with the Functional API, you break the process into discrete units of work called tasks. You compose those tasks inside an entrypoint using ordinary language control flow (if/else, loops, and function calls). Values pass between tasks as return values, not through a shared graph state schema. This walkthrough builds a customer support email agent with the Functional API. For the same scenario with the Graph API, see Thinking in the Graph API. To choose between the APIs, see Choosing between Graph and Functional APIs.

Start with the process you want to automate

Imagine that you need to build an AI agent that handles customer support emails. Your product team has given you these requirements:
To implement an agent with the Functional API, follow these five steps. Steps 1 and 2 are planning. Steps 3 through 5 turn that plan into types, tasks, and an entrypoint you can run.

Step 1: Map out your workflow as discrete steps

Start by identifying the distinct steps in your process. Each step becomes a task (a function that does one specific thing). Then sketch how the entrypoint calls those tasks and branches. Do this before writing code. Functional API control flow: gather and draft on the left, review and send on the right That sketch mirrors the entrypoint: sequential calls and if/else on the left, then human review on the right. The Graph API would encode the same paths as nodes and edges. Now that the components are identified, clarify what each step needs to do:
  • Read Email: Extract and parse the email content
  • Classify Intent: Use an LLM to categorize urgency and topic
  • Doc Search: Query your knowledge base for relevant information
  • Bug Track: Create or update an issue in a tracking system
  • Draft Reply: Generate an appropriate response
  • Human Review: Escalate to a human agent for approval or handling
  • Send Reply: Dispatch the email response
Some steps always run in sequence (Read Email then Classify Intent). Others are conditional (Doc Search for questions and feature requests, Bug Track only for bugs). Encode those branches with ordinary control flow in the entrypoint.

Step 2: Identify what each step needs to do

For each step from Step 1, decide what kind of work it is and what inputs it needs. Use the categories below, then expand the email-agent examples for concrete fields and retry choices.

LLM steps

Use when you need to understand, analyze, generate text, or make reasoning decisions

Data steps

Use when you need to retrieve information from external sources

Action steps

Use when you need to perform external actions

User input steps

Use when you need human intervention

LLM steps

When a step needs to understand, analyze, generate text, or make reasoning decisions:
  • Static context (prompt): Classification categories, urgency definitions, response format
  • Dynamic context (arguments): Email content, sender information
  • Desired outcome: Structured classification that drives later branches
  • Static context (prompt): Tone guidelines, company policies, response templates
  • Dynamic context (arguments): Classification results, search results
  • Desired outcome: Professional email response ready for review

Data steps

When a step needs to retrieve information from external sources:
  • Parameters: Query built from intent and topic
  • Retry strategy: Yes, with exponential backoff for transient failures
  • Caching: Could cache common queries to reduce API calls

Action steps

When a step needs to perform an external action:
  • When to execute: After approval (human or automated)
  • Retry strategy: Yes, with exponential backoff for network issues
  • Should not cache: Each send is a unique action
  • When to execute: Always when intent is “bug”
  • Retry strategy: Yes, critical to not lose bug reports
  • Returns: Ticket ID to include in the response

User input steps

When a step needs human intervention:
  • Context for decision: Original email, draft response, urgency, classification
  • Expected input format: Approval boolean plus optional edited response
  • When triggered: High urgency, complex issues, or quality concerns

Step 3: Decide what data to pass between tasks

In the Functional API, there is no shared graph state schema. Each task takes arguments and returns a value. The entrypoint holds intermediate results in local variables and passes them to the next task. Ask yourself these questions about each piece of data:

Share it across tasks

Does a later task need this value? If yes, return it from the task that creates it, store it in an entrypoint local variable, then pass it as an argument to the task that needs it.

Keep it inside one task

Is it only used to format a prompt or make a call inside one task? If yes, keep it local to that task. Do not return it.
For the email agent, the entrypoint needs to carry:
  • The original email and sender info (inputs to the workflow)
  • Classification results (needed by later branches and the draft step)
  • Search results (expensive to re-fetch)
  • The draft response (needed through review and send)
Store and pass raw data, not formatted prompt text. Format prompts inside the task that calls the model. That keeps tasks reusable and makes checkpoints easier to inspect.
Define types for the values tasks exchange. These are ordinary TypedDicts or TypeScript types, not a Graph API state schema. Add them to your file now; the tasks and entrypoint in the next steps use them.

Step 4: Build your tasks

Turn each Step 1 unit of work into a task. Tasks checkpoint independently, can retry on failure, and return values the entrypoint can await. A task is a function that does one unit of work and returns a result. Call it from an entrypoint; the call returns a promise you can await.

Handle errors appropriately

Before you fill in task bodies, consider how each task can fail. Attach retries on the task for transient errors. Use interrupt() in the entrypoint for human input. Use this table as a guide:

Implementing the email agent tasks

Add one task per step from Step 1 (except human review, which uses interrupt() in the entrypoint in Step 5). The stubs below show the function signatures, return shapes, and retry policies for the email agent. Replace the placeholder bodies with your real LLM calls and API integrations. Put these tasks in the same file as the types from Step 3. read_email is a passthrough here. Keeping it as its own task still gives you a checkpoint before classification and a place to add real MIME parsing later.
You now have callable tasks. Next, compose them in an entrypoint.

Step 5: Compose the entrypoint

The entrypoint is the workflow. Add it below the tasks. It calls those tasks in order, branches with if/else (the control flow from Step 1), and pauses for humans with interrupt(). Provide a checkpointer when you need durable execution and human-in-the-loop.

Try out your agent

After the entrypoint is defined, invoke it with a sample email. With the placeholder classifier above, this urgent billing message is treated as high urgency, so the run hits interrupt() and you can practice resume. Use the same thread_id for the first invoke and the resume invoke.
The workflow pauses at interrupt(), saves completed task results to the checkpointer, and waits. When you resume with Command(resume=...), completed tasks reload from the checkpoint instead of re-running. The thread_id keeps this conversation’s state together.

Summary and next steps

Key insights

Building this email agent shows the Functional API way of thinking:

Break into discrete tasks

Each task does one thing well. Task boundaries are where results checkpoint, so failures and resumes redo less work.

Pass values, do not share graph state

Return raw data from tasks and pass it as arguments. Keep prompt formatting inside the task that needs it.

Entrypoints own control flow

Use if/else, loops, and calls. You do not declare edges or conditional edge maps.

Errors are part of the flow

Transient failures get task retry policies, user-fixable problems pause with interrupt(), and unexpected errors fail the run.

Human input is first-class

interrupt() pauses execution, saves completed task results, and resumes when you provide input.

Composition stays in code

The workflow reads like a normal function. That fits existing procedural code and fast iteration.

Task granularity trade-offs

Most applications can use the breakdown above. Read this section when you are deciding whether to merge or split tasks. LangGraph checkpoints at task boundaries inside an entrypoint. When a run resumes after an interrupt or failure, completed task results load from the checkpoint. Work that lived in the entrypoint body outside a task may run again. Why this breakdown fits the email agent:
  • Isolation of external services: Doc search and bug tracking are separate tasks so retries and failures stay scoped to those API calls.
  • Visibility: Classification is its own task, so you can inspect the structured result before branching.
  • Different failure modes: LLM calls, database lookups, and email sends get different retry policies.
  • Testing: Smaller tasks are easier to unit test and reuse.
Merging read_email and classify_intent into one task is valid. You lose a checkpoint between them and repeat both on failure. For most agents, the observability benefit of separate tasks is worth it.

Where to go from here

This walkthrough introduces Functional API design. Extend it with:

Build checklist

Decision guide for workflows, tasks, streaming, interrupts, and memory

Thinking in the Graph API

The same email agent with nodes, edges, and shared state

Compose workflows

Parallel tasks, calling graphs, and nesting entrypoints

Configure tasks

Retries, timeouts, caching, and resume after errors

Human-in-the-loop

Interrupt patterns for approval and tool review

Add memory

Short-term checkpointers and long-term stores