From 11077affd61c7e2d17b24fbf7fdf9afc2e52394b Mon Sep 17 00:00:00 2001 From: Hunter Lovell Date: Wed, 16 Jul 2025 18:27:05 -0700 Subject: [PATCH] hunter changes over sample --- docs/docs/concepts/application_structure.md | 58 ++- docs/docs/concepts/auth.md | 511 +++++++++----------- docs/docs/concepts/durable_execution.md | 149 +++++- docs/docs/concepts/faq.md | 10 +- docs/docs/concepts/functional_api.md | 329 +++++++------ 5 files changed, 580 insertions(+), 477 deletions(-) diff --git a/docs/docs/concepts/application_structure.md b/docs/docs/concepts/application_structure.md index a383eb4ac..308ea0e65 100644 --- a/docs/docs/concepts/application_structure.md +++ b/docs/docs/concepts/application_structure.md @@ -58,9 +58,13 @@ Below are examples of directory structures for applications: ├── langgraph.json # configuration file for LangGraph └── pyproject.toml # dependencies for your project ``` + +::: + ::: :::js + ```plaintext my-app/ ├── src # all project code lies within here @@ -73,6 +77,7 @@ my-app/ ├── .env # environment variables └── langgraph.json # configuration file for LangGraph ``` + ::: !!! note @@ -92,42 +97,41 @@ See the [LangGraph configuration file reference](../cloud/reference/cli.md#confi ### Examples :::python -* The dependencies involve a custom local package and the `langchain_openai` package. -* A single graph will be loaded from the file `./your_package/your_file.py` with the variable `variable`. -* The environment variables are loaded from the `.env` file. + +- The dependencies involve a custom local package and the `langchain_openai` package. +- A single graph will be loaded from the file `./your_package/your_file.py` with the variable `variable`. +- The environment variables are loaded from the `.env` file. ```json { - "dependencies": [ - "langchain_openai", - "./your_package" - ], - "graphs": { - "my_agent": "./your_package/your_file.py:agent" - }, - "env": "./.env" + "dependencies": ["langchain_openai", "./your_package"], + "graphs": { + "my_agent": "./your_package/your_file.py:agent" + }, + "env": "./.env" } ``` + ::: :::js -* The dependencies will be loaded from a dependency file in the local directory (e.g., `package.json`). -* A single graph will be loaded from the file `./your_package/your_file.js` with the function `agent`. -* The environment variable `OPENAI_API_KEY` is set inline. + +- The dependencies will be loaded from a dependency file in the local directory (e.g., `package.json`). +- A single graph will be loaded from the file `./your_package/your_file.js` with the function `agent`. +- The environment variable `OPENAI_API_KEY` is set inline. ```json { - "dependencies": [ - "." - ], - "graphs": { - "my_agent": "./your_package/your_file.js:agent" - }, - "env": { - "OPENAI_API_KEY": "secret-key" - } + "dependencies": ["."], + "graphs": { + "my_agent": "./your_package/your_file.js:agent" + }, + "env": { + "OPENAI_API_KEY": "secret-key" + } } ``` + ::: ## Dependencies @@ -143,12 +147,14 @@ A LangGraph application may depend on other TypeScript/JavaScript libraries. You will generally need to specify the following information for dependencies to be set up correctly: :::python + 1. A file in the directory that specifies the dependencies (e.g. `requirements.txt`, `pyproject.toml`, or `package.json`). -::: + ::: :::js + 1. A file in the directory that specifies the dependencies (e.g. `package.json`). -::: + ::: 2. A `dependencies` key in the [LangGraph configuration file](#configuration-file-concepts) that specifies the dependencies required to run the LangGraph application. 3. Any additional binaries or system libraries can be specified using `dockerfile_lines` key in the [LangGraph configuration file](#configuration-file-concepts). @@ -163,4 +169,4 @@ You can specify one or more graphs in the configuration file. Each graph is iden If you're working with a deployed LangGraph application locally, you can configure environment variables in the `env` key of the [LangGraph configuration file](#configuration-file-concepts). -For a production deployment, you will typically want to configure the environment variables in the deployment environment. \ No newline at end of file +For a production deployment, you will typically want to configure the environment variables in the deployment environment. diff --git a/docs/docs/concepts/auth.md b/docs/docs/concepts/auth.md index 9cfe54bcf..45ab2c210 100644 --- a/docs/docs/concepts/auth.md +++ b/docs/docs/concepts/auth.md @@ -35,7 +35,7 @@ LangGraph Platform provides different security defaults: - Can be customized with your auth handler !!! note "Custom auth" - Custom auth **is supported** for all plans in LangGraph Platform. +Custom auth **is supported** for all plans in LangGraph Platform. ### Self-Hosted @@ -44,8 +44,8 @@ LangGraph Platform provides different security defaults: - You control all aspects of authentication and authorization !!! note "Custom auth" - Custom auth is supported for **Enterprise** self-hosted deployments. - Standalone Container (Lite) deployments do not support custom auth natively. +Custom auth is supported for **Enterprise** self-hosted deployments. +Standalone Container (Lite) deployments do not support custom auth natively. ## System Architecture @@ -53,24 +53,24 @@ A typical authentication setup involves three main components: 1. **Authentication Provider** (Identity Provider/IdP) - * A dedicated service that manages user identities and credentials - * Handles user registration, login, password resets, etc. - * Issues tokens (JWT, session tokens, etc.) after successful authentication - * Examples: Auth0, Supabase Auth, Okta, or your own auth server + - A dedicated service that manages user identities and credentials + - Handles user registration, login, password resets, etc. + - Issues tokens (JWT, session tokens, etc.) after successful authentication + - Examples: Auth0, Supabase Auth, Okta, or your own auth server 2. **LangGraph Backend** (Resource Server) - * Your LangGraph application that contains business logic and protected resources - * Validates tokens with the auth provider - * Enforces access control based on user identity and permissions - * Doesn't store user credentials directly + - Your LangGraph application that contains business logic and protected resources + - Validates tokens with the auth provider + - Enforces access control based on user identity and permissions + - Doesn't store user credentials directly 3. **Client Application** (Frontend) - * Web app, mobile app, or API client - * Collects time-sensitive user credentials and sends to auth provider - * Receives tokens from auth provider - * Includes these tokens in requests to LangGraph backend + - Web app, mobile app, or API client + - Collects time-sensitive user credentials and sends to auth provider + - Receives tokens from auth provider + - Includes these tokens in requests to LangGraph backend Here's how these components typically interact: @@ -95,19 +95,17 @@ Your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_s ::: :::js -Your [`@auth.authenticate`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.authenticate) handler in LangGraph handles steps 4-6, while your [`@auth.on`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.on) handlers implement step 7. +Your [`@auth.authenticate`]() handler in LangGraph handles steps 4-6, while your [`@auth.on`]() handlers implement step 7. ::: ## Authentication -Authentication in LangGraph runs as middleware on every request. Your authentication handler receives request information and should: +:::python +Authentication in LangGraph runs as middleware on every request. Your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler receives request information and should: 1. Validate the credentials -2. Return user info containing the user's identity and user information if valid -3. Raise an HTTP exception or throw an error if invalid - -:::python -Your [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler should return [user info](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.MinimalUserDict) and raise an [HTTP exception](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.exceptions.HTTPException) or AssertionError if invalid. +2. Return [user info](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.MinimalUserDict) containing the user's identity and user information if valid +3. Raise an [HTTPException](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.exceptions.HTTPException) or AssertionError if invalid ```python from langgraph_sdk import Auth @@ -141,58 +139,51 @@ The returned user information is available: - To your authorization handlers via [`ctx.user`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.types.AuthContext) - In your application via `config["configuration"]["langgraph_auth_user"]` -::: + ::: :::js -Your [`@auth.authenticate`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.authenticate) handler should return [user info](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.MinimalUserDict) and throw an [HTTP exception](../cloud/reference/sdk/typescript_sdk_ref.md#auth.exceptions.HTTPException) or throw an error if invalid. +Authentication in LangGraph runs as middleware on every request. Your [`authenticate`]() handler receives request information and should: + +1. Validate the credentials +2. Return user information containing the user's identity and user information if valid +3. Raise an [HTTPException]() if invalid ```typescript import { Auth } from "@langchain/langgraph-sdk"; -const auth = new Auth(); +export const auth = new Auth(); auth.authenticate(async (headers: Record) => { - // Validate credentials (e.g., API key, JWT token) - const apiKey = headers["x-api-key"]; - if (!apiKey || !isValidKey(apiKey)) { - throw new Auth.exceptions.HTTPException( - 401, - "Invalid API key" - ); - } + // Validate credentials (e.g., API key, JWT token) + const apiKey = headers["x-api-key"]; + if (!apiKey || !isValidKey(apiKey)) { + throw new Auth.exceptions.HTTPException(401, "Invalid API key"); + } - // Return user info - only identity and isAuthenticated are required - // Add any additional fields you need for authorization - return { - identity: "user-123", // Required: unique user identifier - isAuthenticated: true, // Optional: assumed true by default - permissions: ["read", "write"], // Optional: for permission-based auth - // You can add more custom fields if you want to implement other auth patterns - role: "admin", - orgId: "org-456" - }; + // Return user info - only identity and isAuthenticated are required + // Add any additional fields you need for authorization + return { + identity: "user-123", // Required: unique user identifier + isAuthenticated: true, // Optional: assumed true by default + permissions: ["read", "write"], // Optional: for permission-based auth + // You can add more custom fields if you want to implement other auth patterns + role: "admin", + orgId: "org-456", + }; }); ``` The returned user information is available: -- To your authorization handlers via [`ctx.user`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.AuthContext) +- To your authorization handlers via the `user` property in a [callback handler](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#on) - In your application via `config.configurable.langgraph_auth_user` -::: + ::: ??? tip "Supported Parameters" - The authentication handler can accept any of the following parameters by name: - - * request: The raw request object - * body: The parsed request body - * path: The request path - * method: The HTTP method - * query_params/queryParams: URL query parameters - * headers: Request headers - * authorization: The Authorization header value - :::python + The [`@auth.authenticate`](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk.auth.Auth.authenticate) handler can accept any of the following parameters by name: + * request (Request): The raw ASGI request object * body (dict): The parsed request body * path (str): The request path, e.g., "/threads/abcd-1234-abcd-1234/runs/abcd-1234-abcd-1234/stream" @@ -204,6 +195,8 @@ The returned user information is available: ::: :::js + The [`authenticate`]() handler can accept any of the following parameters: + * request (Request): The raw request object * body (object): The parsed request body * path (string): The request path, e.g., "/threads/abcd-1234-abcd-1234/runs/abcd-1234-abcd-1234/stream" @@ -213,7 +206,7 @@ The returned user information is available: * headers (Record): Request headers * authorization (string | null): The Authorization header value (e.g., "Bearer ") ::: - + In many of our tutorials, we will just show the "authorization" parameter to be concise, but you can opt to accept more information as needed to implement your custom authentication scheme. @@ -223,7 +216,7 @@ After authentication, LangGraph calls your authorization handlers to control acc 1. Add metadata to be saved during resource creation by mutating the metadata. See the [supported actions table](#supported-actions) for the list of types the value can take for each action. 2. Filter resources by metadata during search/list or read operations by returning a [filter](#filter-operations). -3. Raise an HTTP exception or throw an error if access is denied. +3. Raise an HTTP exception if access is denied. If you want to just implement simple user-scoped access control, you can use a single authorization handler for all resources and actions. If you want to have different control depending on the resource and action, you can use [resource-specific handlers](#resource-specific-handlers). See the [Supported Resources](#supported-resources) section for a full list of the resources that support access control. @@ -269,53 +262,38 @@ async def add_owner( # to ensure users can only access their own resources return filters ``` + ::: :::js -Your [`@auth.on`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.on) handlers control access by mutating the `value.metadata` object directly and returning a [filter object](#filter-operations). +You can granularly control access by mutating the `value.metadata` object directly and returning a [filter object](#filter-operations) when registering an [`on()`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#on) handler. ```typescript -auth.on(async ( - ctx: Auth.types.AuthContext, - value: any // The payload being sent to this access method -): Promise => { // Returns a filter object that restricts access to resources - /** - * Authorize all access to threads, runs, crons, and assistants. - * - * This handler does two things: - * - Adds a value to resource metadata (to persist with the resource so it can be filtered later) - * - Returns a filter (to restrict access to existing resources) - * - * Args: - * ctx: Authentication context containing user info, permissions, the path, and - * value: The request payload sent to the endpoint. For creation - * operations, this contains the resource parameters. For read - * operations, this contains the resource being accessed. - * - * Returns: - * A filter object that LangGraph uses to restrict access to resources. - * See [Filter Operations](#filter-operations) for supported operators. - */ +import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth"; + +export const auth = new Auth() + .authenticate(async (request: Request) => ({ + identity: "user-123", + permissions: [], + })) + .on("*", ({ value, user }) => { // Create filter to restrict access to just this user's resources - const filters = { owner: ctx.user.identity }; + const filters = { owner: user.identity }; - // Get or create the metadata object in the payload - // This is where we store persistent info about the resource - if (!value.metadata) { - value.metadata = {}; + // If the operation supports metadata, add the user identity + // as metadata to the resource. + if ("metadata" in value) { + value.metadata ??= {}; + value.metadata.owner = user.identity; } - // Add owner to metadata - if this is a create or update operation, - // this information will be saved with the resource - // So we can filter by it later in read operations - Object.assign(value.metadata, filters); - // Return filters to restrict access // These filters are applied to ALL operations (create, read, update, search, etc.) // to ensure users can only access their own resources return filters; -}); + }); ``` + ::: ### Resource-Specific Handlers {#resource-specific-handlers} @@ -332,6 +310,7 @@ When a request is made, the most specific handler that matches that resource and For a full list of supported resources and actions, see the [Supported Resources](#supported-resources) section below. :::python + ```python # Generic / global handler catches calls that aren't handled by more specific handlers @auth.on @@ -415,121 +394,105 @@ async def on_assistant_create( detail="User lacks the required permissions." ) ``` + ::: :::js + ```typescript -// Generic / global handler catches calls that aren't handled by more specific handlers -auth.on(async (ctx: Auth.types.AuthContext, value: any) => { - console.log(`Request to ${ctx.path} by ${ctx.user.identity}`); - throw new Auth.exceptions.HTTPException( - 403, - "Forbidden" - ); -}); +import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth"; -// Matches the "thread" resource and all actions - create, read, update, delete, search -// Since this is **more specific** than the generic @auth.on handler, it will take precedence -// over the generic handler for all actions on the "threads" resource -auth.on.threads(async ( - ctx: Auth.types.AuthContext, - value: Auth.types.threads.create.value -) => { - if (!ctx.permissions.includes("write")) { - throw new Auth.exceptions.HTTPException( - 403, - "User lacks the required permissions." - ); +export const auth = new Auth() + .authenticate(async (request: Request) => ({ + identity: "user-123", + permissions: ["threads:write", "threads:read"], + })) + .on("*", ({ event, user }) => { + console.log(`Request for ${event} by ${user.identity}`); + throw new HTTPException(403, { message: "Forbidden" }); + }) + + // Matches the "threads" resource and all actions - create, read, update, delete, search + // Since this is **more specific** than the generic `on("*")` handler, it will take precedence over the generic handler for all actions on the "threads" resource + .on("threads", ({ permissions, value, user }) => { + if (!permissions.includes("write")) { + throw new HTTPException(403, { + message: "User lacks the required permissions.", + }); } - // Setting metadata on the thread being created - // will ensure that the resource contains an "owner" field + + // Not all events do include `metadata` property in `value`. + // So we need to add this type guard. + if ("metadata" in value) { + value.metadata ??= {}; + value.metadata.owner = user.identity; + } + + return { owner: user.identity }; + }) + + // Thread creation. This will match only on thread create actions. + // Since this is **more specific** than both the generic `on("*")` handler and the `on("threads")` handler, it will take precedence for any "create" actions on the "threads" resources + .on("threads:create", ({ value, user, permissions }) => { + if (!permissions.includes("write")) { + throw new HTTPException(403, { + message: "User lacks the required permissions.", + }); + } + + // Setting metadata on the thread being created will ensure that the resource contains an "owner" field // Then any time a user tries to access this thread or runs within the thread, // we can filter by owner - if (!value.metadata) { - value.metadata = {}; - } - value.metadata.owner = ctx.user.identity; - return { owner: ctx.user.identity }; -}); + value.metadata ??= {}; + value.metadata.owner = user.identity; -// Thread creation. This will match only on thread create actions -// Since this is **more specific** than both the generic @auth.on handler and the @auth.on.threads handler, -// it will take precedence for any "create" actions on the "threads" resources -auth.on.threads.create(async ( - ctx: Auth.types.AuthContext, - value: Auth.types.threads.create.value -) => { - // Setting metadata on the thread being created - // will ensure that the resource contains an "owner" field - // Then any time a user tries to access this thread or runs within the thread, - // we can filter by owner - if (!value.metadata) { - value.metadata = {}; - } - value.metadata.owner = ctx.user.identity; - return { owner: ctx.user.identity }; -}); + return { owner: user.identity }; + }) -// Reading a thread. Since this is also more specific than the generic @auth.on handler, and the @auth.on.threads handler, -// it will take precedence for any "read" actions on the "threads" resource -auth.on.threads.read(async ( - ctx: Auth.types.AuthContext, - value: Auth.types.threads.read.value -) => { + // Reading a thread. Since this is also more specific than the generic `on("*")` handler, and the `on("threads")` handler, + .on("threads:read", ({ user }) => { // Since we are reading (and not creating) a thread, // we don't need to set metadata. We just need to - // return a filter to ensure users can only see their own threads - return { owner: ctx.user.identity }; -}); + // return a filter to ensure users can only see their own threads. + return { owner: user.identity }; + }) -// Run creation, streaming, updates, etc. -// This takes precedence over the generic @auth.on handler and the @auth.on.threads handler -auth.on.threads.createRun(async ( - ctx: Auth.types.AuthContext, - value: Auth.types.threads.createRun.value -) => { - if (!value.metadata) { - value.metadata = {}; - } - value.metadata.owner = ctx.user.identity; - // Inherit thread's access control - return { owner: ctx.user.identity }; -}); + // Run creation, streaming, updates, etc. + // This takes precedence over the generic `on("*")` handler and the `on("threads")` handler + .on("threads:create_run", ({ value, user }) => { + value.metadata ??= {}; + value.metadata.owner = user.identity; -// Assistant creation -auth.on.assistants.create(async ( - ctx: Auth.types.AuthContext, - value: Auth.types.assistants.create.value -) => { - if (!ctx.permissions.includes("assistants:create")) { - throw new Auth.exceptions.HTTPException( - 403, - "User lacks the required permissions." - ); + return { owner: user.identity }; + }) + + // Assistant creation. This will match only on assistant create actions. + // Since this is **more specific** than both the generic `on("*")` handler and the `on("assistants")` handler, it will take precedence for any "create" actions on the "assistants" resources + .on("assistants:create", ({ value, user, permissions }) => { + if (!permissions.includes("assistants:create")) { + throw new HTTPException(403, { + message: "User lacks the required permissions.", + }); } -}); + + // Setting metadata on the assistant being created will ensure that the resource contains an "owner" field. + // Then any time a user tries to access this assistant, we can filter by owner + value.metadata ??= {}; + value.metadata.owner = user.identity; + + return { owner: user.identity }; + }); ``` + ::: Notice that we are mixing global and resource-specific handlers in the above example. Since each request is handled by the most specific handler, a request to create a `thread` would match the `on_thread_create` handler but NOT the `reject_unhandled_requests` handler. A request to `update` a thread, however would be handled by the global handler, since we don't have a more specific handler for that resource and action. ### Filter Operations {#filter-operations} +:::python Authorization handlers can return different types of values: -- `None`/`null` and `True`/`true` mean "authorize access to all underling resources" -- `False`/`false` means "deny access to all underling resources (raises a 403 exception)" -- A metadata filter restricts access to resources - -A filter is a collection with keys that match the resource metadata. It supports three operators: - -- The default value is a shorthand for exact match, or "$eq", below -- `$eq`: Exact match -- `$contains`: List membership - The value here must be an element of the list. The metadata in the stored resource must be a list/container type. - -A filter with multiple keys is treated using a logical `AND` filter. - -:::python - `None` and `True` mean "authorize access to all underling resources" - `False` means "deny access to all underling resources (raises a 403 exception)" - A metadata filter dictionary will restrict access to resources @@ -545,17 +508,19 @@ See the reference [here](../cloud/reference/sdk/python_sdk_ref.md#langgraph_sdk. ::: :::js +Authorization handlers can return different types of values: + - `null` and `true` mean "authorize access to all underling resources" - `false` means "deny access to all underling resources (raises a 403 exception)" - A metadata filter object will restrict access to resources A filter object is an object with keys that match the resource metadata. It supports three operators: -- The default value is a shorthand for exact match, or "$eq", below. For example, `{owner: userId}` will include only resources with metadata containing `{owner: userId}` -- `$eq`: Exact match (e.g., `{owner: {$eq: userId}}`) - this is equivalent to the shorthand above, `{owner: userId}` -- `$contains`: List membership (e.g., `{allowedUsers: {$contains: userId}}`) The value here must be an element of the list. The metadata in the stored resource must be a list/container type. +- The default value is a shorthand for exact match, or "$eq", below. For example, `{ owner: userId}` will include only resources with metadata containing `{ owner: userId }` +- `$eq`: Exact match (e.g., `{ owner: { $eq: userId } }`) - this is equivalent to the shorthand above, `{ owner: userId }` +- `$contains`: List membership (e.g., `{ allowedUsers: { $contains: userId} }`) The value here must be an element of the list. The metadata in the stored resource must be a list/container type. -An object with multiple keys is treated using a logical `AND` filter. For example, `{owner: orgId, allowedUsers: {$contains: userId}}` will only match resources with metadata whose "owner" is `orgId` and whose "allowedUsers" list contains `userId`. +An object with multiple keys is treated using a logical `AND` filter. For example, `{ owner: orgId, allowedUsers: { $contains: userId} }` will only match resources with metadata whose "owner" is `orgId` and whose "allowedUsers" list contains `userId`. See the reference [here](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.FilterType) for more information. ::: @@ -568,6 +533,7 @@ Here are some typical authorization patterns: This common pattern lets you scope all threads, assistants, crons, and runs to a single user. It's useful for common single-user use cases like regular chatbot-style apps. :::python + ```python @auth.on async def owner_only(ctx: Auth.types.AuthContext, value: dict): @@ -575,18 +541,26 @@ async def owner_only(ctx: Auth.types.AuthContext, value: dict): metadata["owner"] = ctx.user.identity return {"owner": ctx.user.identity} ``` + ::: :::js + ```typescript -auth.on(async (ctx: Auth.types.AuthContext, value: any) => { - if (!value.metadata) { - value.metadata = {}; +export const auth = new Auth() + .authenticate(async (request: Request) => ({ + identity: "user-123", + permissions: ["threads:write", "threads:read"], + })) + .on("*", ({ value, user }) => { + if ("metadata" in value) { + value.metadata ??= {}; + value.metadata.owner = user.identity; } - value.metadata.owner = ctx.user.identity; - return { owner: ctx.user.identity }; -}); + return { owner: user.identity }; + }); ``` + ::: ### Permission-based Access @@ -594,6 +568,7 @@ auth.on(async (ctx: Auth.types.AuthContext, value: any) => { This pattern lets you control access based on **permissions**. It's useful if you want certain roles to have broader or more restricted access to resources. :::python + ```python # In your auth handler: @auth.authenticate @@ -629,62 +604,51 @@ async def rbac_create(ctx: Auth.types.AuthContext, value: dict): ) return _default(ctx, value) ``` + ::: :::js + ```typescript -// In your auth handler: -auth.authenticate(async (headers: Record) => { - // ... - return { - identity: "user-123", - isAuthenticated: true, - permissions: ["threads:write", "threads:read"] // Define permissions in auth - }; -}); +import { Auth, HTTPException } from "@langchain/langgraph-sdk/auth"; -function _default(ctx: Auth.types.AuthContext, value: any) { - if (!value.metadata) { - value.metadata = {}; +export const auth = new Auth() + .authenticate(async (request: Request) => ({ + identity: "user-123", + // Define permissions in auth + permissions: ["threads:write", "threads:read"], + })) + .on("threads:create", ({ value, user, permissions }) => { + if (!permissions.includes("threads:write")) { + throw new HTTPException(403, { message: "Unauthorized" }); } - value.metadata.owner = ctx.user.identity; - return { owner: ctx.user.identity }; -} -auth.on.threads.create(async (ctx: Auth.types.AuthContext, value: any) => { - if (!ctx.permissions.includes("threads:write")) { - throw new Auth.exceptions.HTTPException( - 403, - "Unauthorized" - ); + if ("metadata" in value) { + value.metadata ??= {}; + value.metadata.owner = user.identity; + } + return { owner: user.identity }; + }) + .on("threads:read", ({ user, permissions }) => { + if ( + !permissions.includes("threads:read") && + !permissions.includes("threads:write") + ) { + throw new HTTPException(403, { message: "Unauthorized" }); } - return _default(ctx, value); -}); -auth.on.threads.read(async (ctx: Auth.types.AuthContext, value: any) => { - if (!ctx.permissions.includes("threads:read") && !ctx.permissions.includes("threads:write")) { - throw new Auth.exceptions.HTTPException( - 403, - "Unauthorized" - ); - } - return _default(ctx, value); -}); + return { owner: user.identity }; + }); ``` + ::: ## Supported Resources LangGraph provides three levels of authorization handlers, from most general to most specific: -1. **Global Handler**: Matches all resources and actions -2. **Resource Handler**: Matches all actions for a specific resource -3. **Action Handler**: Matches a specific action on a specific resource - -The most specific matching handler will be used. For example, a thread-specific create handler takes precedence over a general threads handler for thread creation. -If a more specific handler is registered, the more general handler will not be called for that resource and action. - :::python + 1. **Global Handler** (`@auth.on`): Matches all resources and actions 2. **Resource Handler** (e.g., `@auth.on.threads`, `@auth.on.assistants`, `@auth.on.crons`): Matches all actions for a specific resource 3. **Action Handler** (e.g., `@auth.on.threads.create`, `@auth.on.threads.read`): Matches a specific action on a specific resource @@ -694,18 +658,19 @@ If a more specific handler is registered, the more general handler will not be c ::: :::js -1. **Global Handler** (`auth.on`): Matches all resources and actions -2. **Resource Handler** (e.g., `auth.on.threads`, `auth.on.assistants`, `auth.on.crons`): Matches all actions for a specific resource -3. **Action Handler** (e.g., `auth.on.threads.create`, `auth.on.threads.read`): Matches a specific action on a specific resource -The most specific matching handler will be used. For example, `auth.on.threads.create` takes precedence over `auth.on.threads` for thread creation. +1. **Global Handler** (`on("*")`): Matches all resources and actions +2. **Resource Handler** (e.g., `on("threads")`, `on("assistants")`, `on("crons")`): Matches all actions for a specific resource +3. **Action Handler** (e.g., `on("threads:create")`, `on("threads:read")`): Matches a specific action on a specific resource + +The most specific matching handler will be used. For example, `on("threads:create")` takes precedence over `on("threads")` for thread creation. If a more specific handler is registered, the more general handler will not be called for that resource and action. ::: +:::python ???+ tip "Type Safety" - Each handler has type hints available for its `value` parameter. For example: - - :::python +Each handler has type hints available for its `value` parameter. For example: + ```python @auth.on.threads.create async def on_thread_create( @@ -713,14 +678,14 @@ If a more specific handler is registered, the more general handler will not be c value: Auth.types.on.threads.create.value # Specific type for thread creation ): ... - + @auth.on.threads async def on_threads( ctx: Auth.types.AuthContext, value: Auth.types.on.threads.value # Union type of all thread actions ): ... - + @auth.on async def on_all( ctx: Auth.types.AuthContext, @@ -730,34 +695,12 @@ If a more specific handler is registered, the more general handler will not be c ``` ::: - :::js - ```typescript - auth.on.threads.create(async ( - ctx: Auth.types.AuthContext, - value: Auth.types.on.threads.create.value // Specific type for thread creation - ) => { - // ... - }); - - auth.on.threads(async ( - ctx: Auth.types.AuthContext, - value: Auth.types.on.threads.value // Union type of all thread actions - ) => { - // ... - }); - - auth.on(async ( - ctx: Auth.types.AuthContext, - value: any // Union type of all possible actions - ) => { - // ... - }); - ``` - ::: - More specific handlers provide better type hints since they handle fewer action types. +::: + #### Supported actions and types {#supported-actions} + Here are all the supported action handlers: :::python @@ -782,24 +725,24 @@ Here are all the supported action handlers: ::: :::js -| Resource | Handler | Description | Value Type | -|----------|---------|-------------|------------| -| **Threads** | `auth.on.threads.create` | Thread creation | [`ThreadsCreate`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.ThreadsCreate) | -| | `auth.on.threads.read` | Thread retrieval | [`ThreadsRead`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.ThreadsRead) | -| | `auth.on.threads.update` | Thread updates | [`ThreadsUpdate`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.ThreadsUpdate) | -| | `auth.on.threads.delete` | Thread deletion | [`ThreadsDelete`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.ThreadsDelete) | -| | `auth.on.threads.search` | Listing threads | [`ThreadsSearch`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.ThreadsSearch) | -| | `auth.on.threads.createRun` | Creating or updating a run | [`RunsCreate`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.RunsCreate) | -| **Assistants** | `auth.on.assistants.create` | Assistant creation | [`AssistantsCreate`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.AssistantsCreate) | -| | `auth.on.assistants.read` | Assistant retrieval | [`AssistantsRead`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.AssistantsRead) | -| | `auth.on.assistants.update` | Assistant updates | [`AssistantsUpdate`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.AssistantsUpdate) | -| | `auth.on.assistants.delete` | Assistant deletion | [`AssistantsDelete`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.AssistantsDelete) | -| | `auth.on.assistants.search` | Listing assistants | [`AssistantsSearch`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.AssistantsSearch) | -| **Crons** | `auth.on.crons.create` | Cron job creation | [`CronsCreate`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.CronsCreate) | -| | `auth.on.crons.read` | Cron job retrieval | [`CronsRead`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.CronsRead) | -| | `auth.on.crons.update` | Cron job updates | [`CronsUpdate`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.CronsUpdate) | -| | `auth.on.crons.delete` | Cron job deletion | [`CronsDelete`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.CronsDelete) | -| | `auth.on.crons.search` | Listing cron jobs | [`CronsSearch`](../cloud/reference/sdk/typescript_sdk_ref.md#auth.types.CronsSearch) | +| Resource | Event | Description | Value Type | +| -------------- | -------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| **Threads** | `threads:create` | Thread creation | [`ThreadsCreate`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#threadscreate) | +| | `threads:read` | Thread retrieval | [`ThreadsRead`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#threadsread) | +| | `threads:update` | Thread updates | [`ThreadsUpdate`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#threadsupdate) | +| | `threads:delete` | Thread deletion | [`ThreadsDelete`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#threadsdelete) | +| | `threads:search` | Listing threads | [`ThreadsSearch`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#threadssearch) | +| | `threads:create_run` | Creating or updating a run | [`RunsCreate`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#threadscreate_run) | +| **Assistants** | `assistants:create` | Assistant creation | [`AssistantsCreate`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#assistantscreate) | +| | `assistants:read` | Assistant retrieval | [`AssistantsRead`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#assistantsread) | +| | `assistants:update` | Assistant updates | [`AssistantsUpdate`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#assistantsupdate) | +| | `assistants:delete` | Assistant deletion | [`AssistantsDelete`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#assistantsdelete) | +| | `assistants:search` | Listing assistants | [`AssistantsSearch`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#assistantssearch) | +| **Crons** | `crons:create` | Cron job creation | [`CronsCreate`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#cronscreate) | +| | `crons:read` | Cron job retrieval | [`CronsRead`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#cronsread) | +| | `crons:update` | Cron job updates | [`CronsUpdate`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#cronsupdate) | +| | `crons:delete` | Cron job deletion | [`CronsDelete`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#cronsdelete) | +| | `crons:search` | Listing cron jobs | [`CronsSearch`](https://langchain-ai.github.io/langgraph/cloud/reference/sdk/js_ts_sdk_ref/#cronssearch) | ::: ???+ note "About Runs" @@ -811,7 +754,7 @@ Here are all the supported action handlers: ::: :::js - There is a specific `createRun` handler for creating new runs because it had more arguments that you can view in the handler. + There is a specific `threads:create_run` handler for creating new runs because it had more arguments that you can view in the handler. ::: ## Next Steps @@ -819,4 +762,4 @@ Here are all the supported action handlers: For implementation details: - Check out the introductory tutorial on [setting up authentication](../tutorials/auth/getting_started.md) -- See the how-to guide on implementing a [custom auth handlers](../how-tos/auth/custom_auth.md) \ No newline at end of file +- See the how-to guide on implementing a [custom auth handlers](../how-tos/auth/custom_auth.md) diff --git a/docs/docs/concepts/durable_execution.md b/docs/docs/concepts/durable_execution.md index ea0a38c03..7ce20a86f 100644 --- a/docs/docs/concepts/durable_execution.md +++ b/docs/docs/concepts/durable_execution.md @@ -5,7 +5,7 @@ search: # Durable Execution -**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](./human_in_the_loop.md), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later). +**Durable execution** is a technique in which a process or workflow saves its progress at key points, allowing it to pause and later resume exactly where it left off. This is particularly useful in scenarios that require [human-in-the-loop](./human_in_the_loop.md), where users can inspect, validate, or modify the process before continuing, and in long-running tasks that might encounter interruptions or errors (e.g., calls to an LLM timing out). By preserving completed work, durable execution enables a process to resume without reprocessing previous steps -- even after a significant delay (e.g., a week later). LangGraph's built-in [persistence](./persistence.md) layer provides durable execution for workflows, ensuring that the state of each execution step is saved to a durable store. This capability guarantees that if a workflow is interrupted -- whether by a system failure or for [human-in-the-loop](./human_in_the_loop.md) interactions -- it can be resumed from its last recorded state. @@ -20,7 +20,12 @@ To leverage durable execution in LangGraph, you need to: 1. Enable [persistence](./persistence.md) in your workflow by specifying a [checkpointer](./persistence.md#checkpointer-libraries) that will save workflow progress. 2. Specify a [thread identifier](./persistence.md#threads) when executing a workflow. This will track the execution history for a particular instance of the workflow. -3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside [tasks][langgraph.func.task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay). + +:::python 3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside [tasks][langgraph.func.task] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay). +::: + +:::js 3. Wrap any non-deterministic operations (e.g., random number generation) or operations with side effects (e.g., file writes, API calls) inside [tasks][] to ensure that when a workflow is resumed, these operations are not repeated for the particular run, and instead their results are retrieved from the persistence layer. For more information, see [Determinism and Consistent Replay](#determinism-and-consistent-replay). +::: ## Determinism and Consistent Replay @@ -30,17 +35,25 @@ As a result, when you are writing a workflow for durable execution, you must wra To ensure that your workflow is deterministic and can be consistently replayed, follow these guidelines: -- **Avoid Repeating Work**: If a [node](./low_level.md#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer. -- **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes. +- **Avoid Repeating Work**: If a [node](./low_level.md#nodes) contains multiple operations with side effects (e.g., logging, file writes, or network calls), wrap each operation in a separate **task**. This ensures that when the workflow is resumed, the operations are not repeated, and their results are retrieved from the persistence layer. +- **Encapsulate Non-Deterministic Operations:** Wrap any code that might yield non-deterministic results (e.g., random number generation) inside **tasks** or **nodes**. This ensures that, upon resumption, the workflow follows the exact recorded sequence of steps with the same outcomes. - **Use Idempotent Operations**: When possible ensure that side effects (e.g., API calls, file writes) are idempotent. This means that if an operation is retried after a failure in the workflow, it will have the same effect as the first time it was executed. This is particularly important for operations that result in data writes. In the event that a **task** starts but fails to complete successfully, the workflow's resumption will re-run the **task**, relying on recorded outcomes to maintain consistency. Use idempotency keys or verify existing results to avoid unintended duplication, ensuring a smooth and predictable workflow execution. +:::python For some examples of pitfalls to avoid, see the [Common Pitfalls](./functional_api.md#common-pitfalls) section in the functional API, which shows how to structure your code using **tasks** to avoid these issues. The same principles apply to the [StateGraph (Graph API)][langgraph.graph.state.StateGraph]. +::: + +:::js +For some examples of pitfalls to avoid, see the [Common Pitfalls](./functional_api.md#common-pitfalls) section in the functional API, which shows +how to structure your code using **tasks** to avoid these issues. The same principles apply to the [StateGraph (Graph API)][]. +::: ## Using tasks in nodes If a [node](./low_level.md#nodes) contains multiple operations, you may find it easier to convert each operation into a **task** rather than refactor the operations into individual nodes. +:::python === "Original" ```python @@ -142,16 +155,136 @@ If a [node](./low_level.md#nodes) contains multiple operations, you may find it graph.invoke({"urls": ["https://www.example.com"]}, config) ``` +::: + +:::js +=== "Original" + + ```typescript + import { StateGraph, START, END } from "@langchain/langgraph"; + import { MemorySaver } from "@langchain/langgraph"; + import { v4 as uuidv4 } from "uuid"; + import { z } from "zod"; + + // Define a Zod schema to represent the state + const State = z.object({ + url: z.string(), + result: z.string().optional(), + }); + + const callApi = async (state: z.infer) => { + // highlight-next-line + const response = await fetch(state.url); + const text = await response.text(); + const result = text.slice(0, 100); // Side-effect + return { + result, + }; + }; + + // Create a StateGraph builder and add a node for the callApi function + const builder = new StateGraph(State) + .addNode("callApi", callApi) + .addEdge(START, "callApi") + .addEdge("callApi", END); + + // Specify a checkpointer + const checkpointer = new MemorySaver(); + + // Compile the graph with the checkpointer + const graph = builder.compile({ checkpointer }); + + // Define a config with a thread ID. + const threadId = uuidv4(); + const config = { configurable: { thread_id: threadId } }; + + // Invoke the graph + await graph.invoke({ url: "https://www.example.com" }, config); + ``` + +=== "With task" + + ```typescript + import { StateGraph, START, END } from "@langchain/langgraph"; + import { MemorySaver } from "@langchain/langgraph"; + import { task } from "@langchain/langgraph"; + import { v4 as uuidv4 } from "uuid"; + import { z } from "zod"; + + // Define a Zod schema to represent the state + const State = z.object({ + urls: z.array(z.string()), + results: z.array(z.string()).optional(), + }); + + const makeRequest = task("makeRequest", async (url: string) => { + // highlight-next-line + const response = await fetch(url); + const text = await response.text(); + return text.slice(0, 100); + }); + + const callApi = async (state: z.infer) => { + // highlight-next-line + const requests = state.urls.map((url) => makeRequest(url)); + const results = await Promise.all(requests); + return { + results, + }; + }; + + // Create a StateGraph builder and add a node for the callApi function + const builder = new StateGraph(State) + .addNode("callApi", callApi) + .addEdge(START, "callApi") + .addEdge("callApi", END); + + // Specify a checkpointer + const checkpointer = new MemorySaver(); + + // Compile the graph with the checkpointer + const graph = builder.compile({ checkpointer }); + + // Define a config with a thread ID. + const threadId = uuidv4(); + const config = { configurable: { thread_id: threadId } }; + + // Invoke the graph + await graph.invoke({ urls: ["https://www.example.com"] }, config); + ``` + +::: + ## Resuming Workflows Once you have enabled durable execution in your workflow, you can resume execution for the following scenarios: +:::python + - **Pausing and Resuming Workflows:** Use the [interrupt][langgraph.types.interrupt] function to pause a workflow at specific points and the [Command][langgraph.types.Command] primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details. - **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `None` as the input value (see this [example](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API). + ::: + +:::js + +- **Pausing and Resuming Workflows:** Use the [interrupt](insert-ref) function to pause a workflow at specific points and the [Command](insert-ref) primitive to resume it with updated state. See [**Human-in-the-Loop**](./human_in_the_loop.md) for more details. +- **Recovering from Failures:** Automatically resume workflows from the last successful checkpoint after an exception (e.g., LLM provider outage). This involves executing the workflow with the same thread identifier by providing it with a `null` as the input value (see this [example](../how-tos/use-functional-api.md#resuming-after-an-error) with the functional API). + ::: ## Starting Points for Resuming Workflows -* If you're using a [StateGraph (Graph API)][langgraph.graph.state.StateGraph], the starting point is the beginning of the [**node**](./low_level.md#nodes) where execution stopped. -* If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted. -Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped. -* If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped. \ No newline at end of file +:::python + +- If you're using a [StateGraph (Graph API)][langgraph.graph.state.StateGraph], the starting point is the beginning of the [**node**](./low_level.md#nodes) where execution stopped. +- If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted. + Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped. +- If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped. + ::: + +:::js + +- If you're using a [StateGraph (Graph API)](./low_level.md), the starting point is the beginning of the [**node**](./low_level.md#nodes) where execution stopped. +- If you're making a subgraph call inside a node, the starting point will be the **parent** node that called the subgraph that was halted. + Inside the subgraph, the starting point will be the specific [**node**](./low_level.md#nodes) where execution stopped. +- If you're using the Functional API, the starting point is the beginning of the [**entrypoint**](./functional_api.md#entrypoint) where execution stopped. + ::: diff --git a/docs/docs/concepts/faq.md b/docs/docs/concepts/faq.md index bd1df3a56..f68292877 100644 --- a/docs/docs/concepts/faq.md +++ b/docs/docs/concepts/faq.md @@ -13,7 +13,7 @@ No. LangGraph is an orchestration framework for complex agentic systems and is m ## How is LangGraph different from other agent frameworks? -Other agentic frameworks can work for simple, generic tasks but fall short for complex tasks bespoke to a company’s needs. LangGraph provides a more expressive framework to handle companies’ unique tasks without restricting users to a single black-box cognitive architecture. +Other agentic frameworks can work for simple, generic tasks but fall short for complex tasks. LangGraph provides a more expressive framework to handle your unique tasks without restricting you to a single black-box cognitive architecture. ## Does LangGraph impact the performance of my app? @@ -28,14 +28,14 @@ Yes. LangGraph is an MIT-licensed open-source library and is free to use. LangGraph is a stateful, orchestration framework that brings added control to agent workflows. LangGraph Platform is a service for deploying and scaling LangGraph applications, with an opinionated API for building agent UXs, plus an integrated developer studio. | Features | LangGraph (open source) | LangGraph Platform | -|---------------------|-----------------------------------------------------------|--------------------------------------------------------------------------------------------------------| +| ------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | Description | Stateful orchestration framework for agentic applications | Scalable infrastructure for deploying LangGraph applications | | SDKs | Python and JavaScript | Python and JavaScript | | HTTP APIs | None | Yes - useful for retrieving & updating state or long-term memory, or creating a configurable assistant | | Streaming | Basic | Dedicated mode for token-by-token messages | | Checkpointer | Community contributed | Supported out-of-the-box | | Persistence Layer | Self-managed | Managed Postgres with efficient storage | -| Deployment | Self-managed | • Cloud SaaS
• Free self-hosted
• Enterprise (paid self-hosted) | +| Deployment | Self-managed | • Cloud SaaS
• Free self-hosted
• Enterprise (paid self-hosted) | | Scalability | Self-managed | Auto-scaling of task queues and servers | | Fault-tolerance | Self-managed | Automated retries | | Concurrency Control | Simple threading | Supports double-texting | @@ -47,7 +47,7 @@ LangGraph is a stateful, orchestration framework that brings added control to ag No. LangGraph Platform is proprietary software. -There is a free, self-hosted version of LangGraph Platform with access to basic features. The Cloud SaaS deployment option is free while in beta, but will eventually be a paid service. We will always give ample notice before charging for a service and reward our early adopters with preferential pricing. The Self-Hosted deployment options are paid services. [Contact our sales team](https://www.langchain.com/contact-sales) to learn more. +There is a free, self-hosted version of LangGraph Platform with access to basic features. The Self-Hosted deployment options are paid services. [Contact our sales team](https://www.langchain.com/contact-sales) to learn more. For more information, see our [LangGraph Platform pricing page](https://www.langchain.com/pricing-langgraph-platform). @@ -67,4 +67,4 @@ If you set an environment variable of `LANGSMITH_TRACING=false`, then no traces ## What does "nodes executed" mean for LangGraph Platform usage? -**Nodes Executed** is the aggregate number of nodes in a LangGraph application that are called and completed successfully during an invocation of the application. If a node in the graph is not called during execution or ends in an error state, these nodes will not be counted. If a node is called and completes successfully multiple times, each occurrence will be counted. \ No newline at end of file +**Nodes Executed** is the aggregate number of nodes in a LangGraph application that are called and completed successfully during an invocation of the application. If a node in the graph is not called during execution or ends in an error state, these nodes will not be counted. If a node is called and completes successfully multiple times, each occurrence will be counted. diff --git a/docs/docs/concepts/functional_api.md b/docs/docs/concepts/functional_api.md index 3f2c31f9a..1eee784cf 100644 --- a/docs/docs/concepts/functional_api.md +++ b/docs/docs/concepts/functional_api.md @@ -9,23 +9,25 @@ search: The **Functional API** allows you to add LangGraph's key features — [persistence](./persistence.md), [memory](../how-tos/memory/add-memory.md), [human-in-the-loop](./human_in_the_loop.md), and [streaming](./streaming.md) — to your applications with minimal changes to your existing code. -It is designed to integrate these features into existing code that may use standard language primitives for branching and control flow, such as `if` statements, `for` loops, and function calls. Unlike many data orchestration frameworks that require restructuring code into an explicit pipeline or DAG, the Functional API allows you to incorporate these capabilities without enforcing a rigid execution model. +It is designed to integrate these features into existing code that may use standard language primitives for branching and control flow, such as `if` statements, `for` loops, and function calls. Unlike many data orchestration frameworks that require restructuring code into an explicit pipeline or DAG, the Functional API allows you to incorporate these capabilities without enforcing a rigid execution model. -The Functional API uses two key building blocks: +The Functional API uses two key building blocks: :::python -- **`@entrypoint`** – Marks a function as the starting point of a workflow, encapsulating logic and managing execution flow, including handling long-running tasks and interrupts. + +- **`@entrypoint`** – Marks a function as the starting point of a workflow, encapsulating logic and managing execution flow, including handling long-running tasks and interrupts. - **`@task`** – Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously within an entrypoint. Tasks return a future-like object that can be awaited or resolved synchronously. -::: + ::: :::js -- **`entrypoint`** – Marks a function as the starting point of a workflow, encapsulating logic and managing execution flow, including handling long-running tasks and interrupts. + +- **`entrypoint`** – An entrypoint encapsulates workflow logic and manages execution flow, including handling long-running tasks and interrupts. - **`task`** – Represents a discrete unit of work, such as an API call or data processing step, that can be executed asynchronously within an entrypoint. Tasks return a future-like object that can be awaited or resolved synchronously. -::: + ::: This provides a minimal abstraction for building workflows with state management and streaming. -!!! tip +!!! tip For users who prefer a more declarative approach, LangGraph's [Graph API](./low_level.md) allows you to define workflows using a Graph paradigm. Both APIs share the same underlying runtime, so you can use them together in the same application. Please see the [Functional API vs. Graph API](#functional-api-vs-graph-api) section for a comparison of the two paradigms. @@ -35,12 +37,12 @@ This provides a minimal abstraction for building workflows with state management Below we demonstrate a simple application that writes an essay and [interrupts](human_in_the_loop.md) to request human review. :::python + ```python from langgraph.checkpoint.memory import MemorySaver from langgraph.func import entrypoint, task from langgraph.types import interrupt - @task def write_essay(topic: str) -> str: """Write an essay about the given topic.""" @@ -66,41 +68,42 @@ def workflow(topic: str) -> dict: "is_approved": is_approved, # Response from HIL } ``` + ::: :::js -```typescript -import { MemorySaver } from "@langchain/langgraph"; -import { entrypoint, task } from "@langchain/langgraph"; -import { interrupt } from "@langchain/langgraph"; -const writeEssay = task("writeEssay", async (topic: string): Promise => { +```typescript +import { MemorySaver, entrypoint, task, interrupt } from "@langchain/langgraph"; + +const writeEssay = task("writeEssay", async (topic: string) => { // A placeholder for a long-running task. - await new Promise(resolve => setTimeout(resolve, 1000)); + await new Promise((resolve) => setTimeout(resolve, 1000)); return `An essay about topic: ${topic}`; }); const workflow = entrypoint( - { checkpointer: new MemorySaver() }, - async (topic: string): Promise<{ essay: string; isApproved: boolean }> => { - const essay = await writeEssay("cat"); + { checkpointer: new MemorySaver(), name: "workflow" }, + async (topic: string) => { + const essay = await writeEssay(topic); const isApproved = interrupt({ // Any json-serializable payload provided to interrupt as argument. // It will be surfaced on the client side as an Interrupt when streaming data // from the workflow. - essay: essay, // The essay we want reviewed. + essay, // The essay we want reviewed. // We can add any additional information that we need. // For example, introduce a key called "action" with some instructions. action: "Please approve/reject the essay", }); return { - essay: essay, // The essay that was generated - isApproved: isApproved, // Response from HIL + essay, // The essay that was generated + isApproved, // Response from HIL }; } ); ``` + ::: ??? example "Detailed Explanation" @@ -183,33 +186,31 @@ const workflow = entrypoint( :::js ```typescript import { v4 as uuidv4 } from "uuid"; - import { MemorySaver } from "@langchain/langgraph"; - import { entrypoint, task } from "@langchain/langgraph"; - import { interrupt } from "@langchain/langgraph"; + import { MemorySaver, entrypoint, task, interrupt } from "@langchain/langgraph"; - const writeEssay = task("writeEssay", async (topic: string): Promise => { + const writeEssay = task("writeEssay", async (topic: string) => { // This is a placeholder for a long-running task. await new Promise(resolve => setTimeout(resolve, 1000)); return `An essay about topic: ${topic}`; }); const workflow = entrypoint( - { checkpointer: new MemorySaver() }, - async (topic: string): Promise<{ essay: string; isApproved: boolean }> => { - const essay = await writeEssay("cat"); + { checkpointer: new MemorySaver(), name: "workflow" }, + async (topic: string) => { + const essay = await writeEssay(topic); const isApproved = interrupt({ // Any json-serializable payload provided to interrupt as argument. // It will be surfaced on the client side as an Interrupt when streaming data // from the workflow. - essay: essay, // The essay we want reviewed. + essay, // The essay we want reviewed. // We can add any additional information that we need. // For example, introduce a key called "action" with some instructions. action: "Please approve/reject the essay", }); return { - essay: essay, // The essay that was generated - isApproved: isApproved, // Response from HIL + essay, // The essay that was generated + isApproved, // Response from HIL }; } ); @@ -229,7 +230,14 @@ const workflow = entrypoint( ```console { writeEssay: 'An essay about topic: cat' } - { __interrupt__: [{ value: { essay: 'An essay about topic: cat', action: 'Please approve/reject the essay' }, resumable: true, ns: ['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], when: 'during' }] } + { + __interrupt__: [{ + value: { essay: 'An essay about topic: cat', action: 'Please approve/reject the essay' }, + resumable: true, + ns: ['workflow:f7b8508b-21c0-8b4c-5958-4e8de74d2684'], + when: 'during' + }] + } ``` An essay has been written and is ready for review. Once the review is provided, we can resume the workflow: @@ -256,17 +264,17 @@ const workflow = entrypoint( ## Entrypoint :::python -The [`@entrypoint`][langgraph.func.entrypoint] decorator can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](./human_in_the_loop.md). +The [`@entrypoint`][langgraph.func.entrypoint] decorator can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling _long-running tasks_ and [interrupts](./human_in_the_loop.md). ::: :::js -The `entrypoint` function can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling *long-running tasks* and [interrupts](./human_in_the_loop.md). +The [`entrypoint`][] function can be used to create a workflow from a function. It encapsulates workflow logic and manages execution flow, including handling _long-running tasks_ and [interrupts](./human_in_the_loop.md). ::: ### Definition :::python -An **entrypoint** is defined by decorating a function with the `@entrypoint` decorator. +An **entrypoint** is defined by decorating a function with the `@entrypoint` decorator. The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use a dictionary as the input type for the first argument. @@ -297,48 +305,33 @@ You will usually want to pass a **checkpointer** to the `@entrypoint` decorator # some logic that may involve long-running tasks like API calls, # and may be interrupted for human-in-the-loop ... - return result + return result ``` + ::: :::js -An **entrypoint** is defined by calling the `entrypoint` function with configuration and a function. +An **entrypoint** is defined by calling the `entrypoint` function with configuration and a function. -The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use a dictionary as the input type for the first argument. +The function **must accept a single positional argument**, which serves as the workflow input. If you need to pass multiple pieces of data, use an object as the input type for the first argument. Creating an entrypoint with a function produces a workflow instance which helps to manage the execution of the workflow (e.g., handles streaming, resumption, and checkpointing). -You will usually want to pass a **checkpointer** to the `entrypoint` function to enable persistence and use features like **human-in-the-loop**. +You will often want to pass a **checkpointer** to the `entrypoint` function to enable persistence and use features like **human-in-the-loop**. -=== "Sync" +```typescript +import { entrypoint } from "@langchain/langgraph"; - ```typescript - import { entrypoint } from "@langchain/langgraph"; +const myWorkflow = entrypoint( + { checkpointer, name: "workflow" }, + async (someInput: Record): Promise => { + // some logic that may involve long-running tasks like API calls, + // and may be interrupted for human-in-the-loop + return result; + } +); +``` - const myWorkflow = entrypoint( - { checkpointer: checkpointer }, - async (someInput: Record): Promise => { - // some logic that may involve long-running tasks like API calls, - // and may be interrupted for human-in-the-loop. - return result; - } - ); - ``` - -=== "Async" - - ```typescript - import { entrypoint } from "@langchain/langgraph"; - - const myWorkflow = entrypoint( - { checkpointer: checkpointer }, - async (someInput: Record): Promise => { - // some logic that may involve long-running tasks like API calls, - // and may be interrupted for human-in-the-loop - return result; - } - ); - ``` ::: !!! important "Serialization" @@ -347,22 +340,24 @@ You will usually want to pass a **checkpointer** to the `entrypoint` function to ### Injectable parameters -When declaring an `entrypoint`, you can request access to additional parameters that will be injected automatically at run time. These parameters include: +When declaring an `entrypoint`, you can request access to additional parameters that will be injected automatically at run time by using the [`getPreviousState()`]() function. These parameters include: :::python -| Parameter | Description | +| Parameter | Description | |--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **previous** | Access the state associated with the previous `checkpoint` for the given thread. See [short-term-memory](#short-term-memory). | -| **store** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for [long-term memory](../how-tos/use-functional-api.md#long-term-memory). | -| **writer** | Use to access the StreamWriter when working with Async Python < 3.11. See [streaming with functional API for details](../how-tos/use-functional-api.md#streaming). | -| **config** | For accessing run time configuration. See [RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) for information. | +| **previous** | Access the state associated with the previous `checkpoint` for the given thread. See [short-term-memory](#short-term-memory). | +| **store** | An instance of [BaseStore][langgraph.store.base.BaseStore]. Useful for [long-term memory](../how-tos/use-functional-api.md#long-term-memory). | +| **writer** | Use to access the StreamWriter when working with Async Python < 3.11. See [streaming with functional API for details](../how-tos/use-functional-api.md#streaming). | +| **config** | For accessing run time configuration. See [RunnableConfig](https://python.langchain.com/docs/concepts/runnables/#runnableconfig) for information. | ::: :::js -| Parameter | Description | -|--------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| **store** | An instance of BaseStore. Useful for [long-term memory](../how-tos/use-functional-api.md#long-term-memory). | -| **config** | For accessing run time configuration. See [LangGraphRunnableConfig](../reference/types.md#langgraphrunnable-config) for information. | +| Parameter | Description | +| ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| **config** | For accessing runtime configuration. Automatically populated as the second argument to the `entrypoint` function (but not `task`, since tasks can have a variable number of arguments). See [RunnableConfig](https://js.langchain.com/docs/concepts/runnables/#runnableconfig) for information. | +| **config.store** | An instance of [BaseStore](/langgraphjs/reference/classes/checkpoint.BaseStore.html). Useful for [long-term memory](#long-term-memory). | +| **config.writer** | A `writer` used for streaming back custom data. See the [guide on streaming custom data](../how-tos/streaming-content.ipynb) | +| **getPreviousState()** | Access the state associated with the previous `checkpoint` for the given thread using [`getPreviousState`](/langgraphjs/reference/functions/langgraph.getPreviousState.html). See [state management](#state-management). | ::: !!! important @@ -383,7 +378,7 @@ When declaring an `entrypoint`, you can request access to additional parameters @entrypoint( checkpointer=checkpointer, # Specify the checkpointer store=in_memory_store # Specify the store - ) + ) def my_workflow( some_input: dict, # The input (e.g., passed via `invoke`) *, @@ -397,22 +392,18 @@ When declaring an `entrypoint`, you can request access to additional parameters :::js ```typescript - import { entrypoint } from "@langchain/langgraph"; - import { BaseStore, InMemoryStore } from "@langchain/langgraph"; - import { LangGraphRunnableConfig } from "@langchain/langgraph"; + import { entrypoint, BaseStore, InMemoryStore, LangGraphRunnableConfig } from "@langchain/langgraph"; const inMemoryStore = new InMemoryStore(); // An instance of InMemoryStore for long-term memory const myWorkflow = entrypoint( { - checkpointer: checkpointer, // Specify the checkpointer + checkpointer, name: "workflow", // Specify the checkpointer store: inMemoryStore, // Specify the store name: "myWorkflow", }, - async ( - someInput: Record, // The input (e.g., passed via `invoke`) - config: LangGraphRunnableConfig // For accessing the configuration passed to the entrypoint - ): Promise => { + async (someInput: Record) => { + const previous = getPreviousState(); // For short-term memory // Rest of workflow logic... } ); @@ -447,7 +438,7 @@ Using the [`@entrypoint`](#entrypoint) yields a [`Pregel`][langgraph.pregel.Preg ``` === "Stream" - + ```python config = { "configurable": { @@ -471,10 +462,11 @@ Using the [`@entrypoint`](#entrypoint) yields a [`Pregel`][langgraph.pregel.Preg async for chunk in my_workflow.astream(some_input, config): print(chunk) ``` + ::: :::js -Using the `entrypoint` yields a workflow object that can be executed using the `invoke` and `stream` methods. +Using the [`entrypoint`](#entrypoint) function will return an object that can be executed using the `invoke` and `stream` methods. === "Invoke" @@ -488,7 +480,7 @@ Using the `entrypoint` yields a workflow object that can be executed using the ` ``` === "Stream" - + ```typescript const config = { configurable: { @@ -500,6 +492,7 @@ Using the `entrypoint` yields a workflow object that can be executed using the ` console.log(chunk); } ``` + ::: ### Resuming @@ -517,7 +510,7 @@ Resuming an execution after an [interrupt][langgraph.types.interrupt] can be don "thread_id": "some_thread_id" } } - + my_workflow.invoke(Command(resume=some_resume_value), config) ``` @@ -531,7 +524,7 @@ Resuming an execution after an [interrupt][langgraph.types.interrupt] can be don "thread_id": "some_thread_id" } } - + await my_workflow.ainvoke(Command(resume=some_resume_value), config) ``` @@ -545,7 +538,7 @@ Resuming an execution after an [interrupt][langgraph.types.interrupt] can be don "thread_id": "some_thread_id" } } - + for chunk in my_workflow.stream(Command(resume=some_resume_value), config): print(chunk) ``` @@ -564,10 +557,11 @@ Resuming an execution after an [interrupt][langgraph.types.interrupt] can be don async for chunk in my_workflow.astream(Command(resume=some_resume_value), config): print(chunk) ``` + ::: :::js -Resuming an execution after an `interrupt` can be done by passing a **resume** value to the `Command` primitive. +Resuming an execution after an [`interrupt`](insert-ref) can be done by passing a **resume** value to the [`Command`](insert-ref) primitive. === "Invoke" @@ -579,7 +573,7 @@ Resuming an execution after an `interrupt` can be done by passing a **resume** v thread_id: "some_thread_id" } }; - + await myWorkflow.invoke(new Command({ resume: someResumeValue }), config); ``` @@ -593,13 +587,21 @@ Resuming an execution after an `interrupt` can be done by passing a **resume** v thread_id: "some_thread_id" } }; - - for await (const chunk of myWorkflow.stream(new Command({ resume: someResumeValue }), config)) { + + const stream = await myWorkflow.stream( + new Command({ resume: someResumableValue }), + config, + ) + + for await (const chunk of stream) { console.log(chunk); } ``` + ::: +:::python + **Resuming after an error** To resume after an error, run the `entrypoint` with a `None` and the same **thread id** (config). @@ -616,7 +618,7 @@ This assumes that the underlying **error** has been resolved and execution can p "thread_id": "some_thread_id" } } - + my_workflow.invoke(None, config) ``` @@ -629,7 +631,7 @@ This assumes that the underlying **error** has been resolved and execution can p "thread_id": "some_thread_id" } } - + await my_workflow.ainvoke(None, config) ``` @@ -642,7 +644,7 @@ This assumes that the underlying **error** has been resolved and execution can p "thread_id": "some_thread_id" } } - + for chunk in my_workflow.stream(None, config): print(chunk) ``` @@ -660,9 +662,17 @@ This assumes that the underlying **error** has been resolved and execution can p async for chunk in my_workflow.astream(None, config): print(chunk) ``` + ::: :::js + +**Resuming after an error** + +To resume after an error, run the `entrypoint` with `null` and the same **thread id** (config). + +This assumes that the underlying **error** has been resolved and execution can proceed successfully. + === "Invoke" ```typescript @@ -671,7 +681,7 @@ This assumes that the underlying **error** has been resolved and execution can p thread_id: "some_thread_id" } }; - + await myWorkflow.invoke(null, config); ``` @@ -683,16 +693,17 @@ This assumes that the underlying **error** has been resolved and execution can p thread_id: "some_thread_id" } }; - + for await (const chunk of myWorkflow.stream(null, config)) { console.log(chunk); } ``` + ::: ### Short-term memory -When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](persistence.md#checkpoints). +When an `entrypoint` is defined with a `checkpointer`, it stores information between successive invocations on the same **thread id** in [checkpoints](persistence.md#checkpoints). :::python This allows accessing the state from the previous invocation using the `previous` parameter. @@ -714,6 +725,7 @@ config = { my_workflow.invoke(1, config) # 1 (previous was None) my_workflow.invoke(2, config) # 3 (previous was 1 from the previous invocation) ``` + ::: :::js @@ -725,8 +737,8 @@ By default, the `getPreviousState` function returns the return value of the prev import { entrypoint, getPreviousState } from "@langchain/langgraph"; const myWorkflow = entrypoint( - { checkpointer: checkpointer }, - async (number: number): Promise => { + { checkpointer, name: "workflow" }, + async (number: number) => { const previous = getPreviousState() ?? 0; return number + previous; } @@ -734,19 +746,20 @@ const myWorkflow = entrypoint( const config = { configurable: { - thread_id: "some_thread_id" - } + thread_id: "some_thread_id", + }, }; -await myWorkflow.invoke(1, config); // 1 (previous was undefined) -await myWorkflow.invoke(2, config); // 3 (previous was 1 from the previous invocation) +await myWorkflow.invoke(1, config); // 1 (previous was undefined) +await myWorkflow.invoke(2, config); // 3 (previous was 1 from the previous invocation) ``` + ::: #### `entrypoint.final` :::python -[entrypoint.final][langgraph.func.entrypoint.final] is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**. +[`entrypoint.final`][langgraph.func.entrypoint.final] is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**. The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. The type annotation is `entrypoint.final[return_type, save_type]`. @@ -755,7 +768,7 @@ The first value is the return value of the entrypoint, and the second value is t def my_workflow(number: int, *, previous: Any = None) -> entrypoint.final[int, int]: previous = previous or 0 # This will return the previous value to the caller, saving - # 2 * number to the checkpoint, which will be used in the next invocation + # 2 * number to the checkpoint, which will be used in the next invocation # for the `previous` parameter. return entrypoint.final(value=previous, save=2 * number) @@ -768,10 +781,11 @@ config = { my_workflow.invoke(3, config) # 0 (previous was None) my_workflow.invoke(1, config) # 6 (previous was 3 * 2 from the previous invocation) ``` + ::: :::js -`entrypoint.final` is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**. +[`entrypoint.final`](insert-ref) is a special primitive that can be returned from an entrypoint and allows **decoupling** the value that is **saved in the checkpoint** from the **return value of the entrypoint**. The first value is the return value of the entrypoint, and the second value is the value that will be saved in the checkpoint. @@ -779,36 +793,37 @@ The first value is the return value of the entrypoint, and the second value is t import { entrypoint, getPreviousState } from "@langchain/langgraph"; const myWorkflow = entrypoint( - { checkpointer: checkpointer }, - async (number: number): Promise => { + { checkpointer, name: "workflow" }, + async (number: number) => { const previous = getPreviousState() ?? 0; // This will return the previous value to the caller, saving - // 2 * number to the checkpoint, which will be used in the next invocation + // 2 * number to the checkpoint, which will be used in the next invocation // for the `previous` parameter. return entrypoint.final({ value: previous, - save: 2 * number + save: 2 * number, }); } ); const config = { configurable: { - thread_id: "1" - } + thread_id: "1", + }, }; -await myWorkflow.invoke(3, config); // 0 (previous was undefined) -await myWorkflow.invoke(1, config); // 6 (previous was 3 * 2 from the previous invocation) +await myWorkflow.invoke(3, config); // 0 (previous was undefined) +await myWorkflow.invoke(1, config); // 6 (previous was 3 * 2 from the previous invocation) ``` + ::: ## Task A **task** represents a discrete unit of work, such as an API call or data processing step. It has two key characteristics: -* **Asynchronous Execution**: Tasks are designed to be executed asynchronously, allowing multiple operations to run concurrently without blocking. -* **Checkpointing**: Task results are saved to a checkpoint, enabling resumption of the workflow from the last saved state. (See [persistence](persistence.md) for more details). +- **Asynchronous Execution**: Tasks are designed to be executed asynchronously, allowing multiple operations to run concurrently without blocking. +- **Checkpointing**: Task results are saved to a checkpoint, enabling resumption of the workflow from the last saved state. (See [persistence](persistence.md) for more details). ### Definition @@ -824,10 +839,11 @@ def slow_computation(input_value): ... return result ``` + ::: :::js -Tasks are defined using the `task` function, which wraps a regular TypeScript function. +Tasks are defined using the `task` function, which wraps a regular function. ```typescript import { task } from "@langchain/langgraph"; @@ -837,6 +853,7 @@ const slowComputation = task("slowComputation", async (inputValue: any) => { return result; }); ``` + ::: !!! important "Serialization" @@ -845,12 +862,12 @@ const slowComputation = task("slowComputation", async (inputValue: any) => { ### Execution -**Tasks** can only be called from within an **entrypoint**, another **task**, or a [state graph node](./low_level.md#nodes). +**Tasks** can only be called from within an **entrypoint**, another **task**, or a [state graph node](./low_level.md#nodes). -Tasks *cannot* be called directly from the main application code. +Tasks _cannot_ be called directly from the main application code. :::python -When you call a **task**, it returns *immediately* with a future object. A future is a placeholder for a result that will be available later. +When you call a **task**, it returns _immediately_ with a future object. A future is a placeholder for a result that will be available later. To obtain the result of a **task**, you can either wait for it synchronously (using `result()`) or await it asynchronously (using `await`). @@ -870,6 +887,7 @@ To obtain the result of a **task**, you can either wait for it synchronously (us async def my_workflow(some_input: int) -> int: return await slow_computation(some_input) # Await result asynchronously ``` + ::: :::js @@ -877,12 +895,13 @@ When you call a **task**, it returns a Promise that can be awaited. ```typescript const myWorkflow = entrypoint( - { checkpointer: checkpointer }, + { checkpointer, name: "workflow" }, async (someInput: number): Promise => { return await slowComputation(someInput); } ); ``` + ::: ## When to use a task @@ -894,7 +913,7 @@ const myWorkflow = entrypoint( - **Parallel Execution**: For I/O-bound tasks, **tasks** enable parallel execution, allowing multiple operations to run concurrently without blocking (e.g., calling multiple APIs). - **Observability**: Wrapping operations in **tasks** provides a way to track the progress of the workflow and monitor the execution of individual operations using [LangSmith](https://docs.smith.langchain.com/). - **Retryable Work**: When work needs to be retried to handle failures or inconsistencies, **tasks** provide a way to encapsulate and manage the retry logic. - + ## Serialization There are two key aspects to serialization in LangGraph: @@ -907,7 +926,7 @@ These requirements are necessary for enabling checkpointing and workflow resumpt ::: :::js -These requirements are necessary for enabling checkpointing and workflow resumption. Use TypeScript primitives like objects, arrays, strings, numbers, and booleans to ensure that your inputs and outputs are serializable. +These requirements are necessary for enabling checkpointing and workflow resumption. Use primitives like objects, arrays, strings, numbers, and booleans to ensure that your inputs and outputs are serializable. ::: Serialization ensures that workflow state, such as task results and intermediate values, can be reliably saved and restored. This is critical for enabling human-in-the-loop interactions, fault tolerance, and parallel execution. @@ -916,9 +935,9 @@ Providing non-serializable inputs or outputs will result in a runtime error when ## Determinism -To utilize features like **human-in-the-loop**, any randomness should be encapsulated inside of **tasks**. This guarantees that when execution is halted (e.g., for human in the loop) and then resumed, it will follow the same *sequence of steps*, even if **task** results are non-deterministic. +To utilize features like **human-in-the-loop**, any randomness should be encapsulated inside of **tasks**. This guarantees that when execution is halted (e.g., for human in the loop) and then resumed, it will follow the same _sequence of steps_, even if **task** results are non-deterministic. -LangGraph achieves this behavior by persisting **task** and [**subgraph**](./subgraphs.md) results as they execute. A well-designed workflow ensures that resuming execution follows the *same sequence of steps*, allowing previously computed results to be retrieved correctly without having to re-execute them. This is particularly useful for long-running **tasks** or **tasks** with non-deterministic results, as it avoids repeating previously done work and allows resuming from essentially the same. +LangGraph achieves this behavior by persisting **task** and [**subgraph**](./subgraphs.md) results as they execute. A well-designed workflow ensures that resuming execution follows the _same sequence of steps_, allowing previously computed results to be retrieved correctly without having to re-execute them. This is particularly useful for long-running **tasks** or **tasks** with non-deterministic results, as it avoids repeating previously done work and allows resuming from essentially the same. While different runs of a workflow can produce different results, resuming a **specific** run should always follow the same sequence of recorded steps. This allows LangGraph to efficiently look up **task** and **subgraph** results that were executed prior to the graph being interrupted and avoid recomputing them. @@ -931,18 +950,20 @@ Idempotency ensures that running the same operation multiple times produces the The **Functional API** and the [Graph APIs (StateGraph)](./low_level.md#stategraph) provide two different paradigms to create applications with LangGraph. Here are some key differences: :::python + - **Control flow**: The Functional API does not require thinking about graph structure. You can use standard Python constructs to define workflows. This will usually trim the amount of code you need to write. -- **Short-term memory**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions. +- **Short-term memory**: The **Graph API** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `@entrypoint` and `@tasks` do not require explicit state management as their state is scoped to the function and is not shared across functions. - **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint. - **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime. -::: + ::: :::js + - **Control flow**: The Functional API does not require thinking about graph structure. You can use standard TypeScript constructs to define workflows. This will usually trim the amount of code you need to write. -- **Short-term memory**: The **GraphAPI** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `entrypoint` and `task` do not require explicit state management as their state is scoped to the function and is not shared across functions. +- **Short-term memory**: The **Graph API** requires declaring a [**State**](./low_level.md#state) and may require defining [**reducers**](./low_level.md#reducers) to manage updates to the graph state. `entrypoint` and `task` do not require explicit state management as their state is scoped to the function and is not shared across functions. - **Checkpointing**: Both APIs generate and use checkpoints. In the **Graph API** a new checkpoint is generated after every [superstep](./low_level.md). In the **Functional API**, when tasks are executed, their results are saved to an existing checkpoint associated with the given entrypoint instead of creating a new checkpoint. - **Visualization**: The Graph API makes it easy to visualize the workflow as a graph which can be useful for debugging, understanding the workflow, and sharing with others. The Functional API does not support visualization as the graph is dynamically generated during runtime. -::: + ::: ## Common Pitfalls @@ -972,11 +993,11 @@ Encapsulate side effects (e.g., writing to a file, sending an email) in tasks to :::js ```typescript import { entrypoint, interrupt } from "@langchain/langgraph"; - import * as fs from "fs"; + import fs from "fs"; const myWorkflow = entrypoint( - { checkpointer: checkpointer }, - async (inputs: Record): Promise => { + { checkpointer, name: "workflow }, + async (inputs: Record) => { // This code will be executed a second time when resuming the workflow. // Which is likely not what you want. fs.writeFileSync("output.txt", "Side effect executed"); @@ -1021,8 +1042,8 @@ Encapsulate side effects (e.g., writing to a file, sending an email) in tasks to }); const myWorkflow = entrypoint( - { checkpointer: checkpointer }, - async (inputs: Record): Promise => { + { checkpointer, name: "workflow" }, + async (inputs: Record) => { // The side effect is now encapsulated in a task. await writeToFile(); const value = interrupt("question"); @@ -1036,8 +1057,8 @@ Encapsulate side effects (e.g., writing to a file, sending an email) in tasks to Operations that might give different results each time (like getting current time or random numbers) should be encapsulated in tasks to ensure that on resume, the same result is returned. -* In a task: Get random number (5) → interrupt → resume → (returns 5 again) → ... -* Not in a task: Get random number (5) → interrupt → resume → get new random number (7) → ... +- In a task: Get random number (5) → interrupt → resume → (returns 5 again) → ... +- Not in a task: Get random number (5) → interrupt → resume → get new random number (7) → ... :::python This is especially important when using **human-in-the-loop** workflows with multiple interrupts calls. LangGraph keeps a list of resume values for each task/entrypoint. When an interrupt is encountered, it's matched with the corresponding resume value. This matching is strictly **index-based**, so the order of the resume values should match the order of the interrupts. @@ -1064,16 +1085,16 @@ Please read the section on [determinism](#determinism) for more details. t0 = inputs["t0"] # highlight-next-line t1 = time.time() - + delta_t = t1 - t0 - + if delta_t > 1: result = slow_task(1).result() value = interrupt("question") else: result = slow_task(2).result() value = interrupt("question") - + return { "result": result, "value": value @@ -1086,12 +1107,12 @@ Please read the section on [determinism](#determinism) for more details. import { entrypoint, interrupt } from "@langchain/langgraph"; const myWorkflow = entrypoint( - { checkpointer: checkpointer }, - async (inputs: { t0: number }): Promise => { + { checkpointer, name: "workflow" }, + async (inputs: { t0: number }) => { const t1 = Date.now(); - + const deltaT = t1 - inputs.t0; - + if (deltaT > 1000) { const result = await slowTask(1); const value = interrupt("question"); @@ -1127,16 +1148,16 @@ Please read the section on [determinism](#determinism) for more details. t0 = inputs["t0"] # highlight-next-line t1 = get_time().result() - + delta_t = t1 - t0 - + if delta_t > 1: result = slow_task(1).result() value = interrupt("question") else: result = slow_task(2).result() value = interrupt("question") - + return { "result": result, "value": value @@ -1145,7 +1166,7 @@ Please read the section on [determinism](#determinism) for more details. ::: :::js - In this example, the workflow uses a task to get the current time. This ensures deterministic behavior on resume. + In this example, the workflow uses the input `t0` to determine which task to execute. This is deterministic because the result of the workflow depends only on the input. ```typescript import { entrypoint, task, interrupt } from "@langchain/langgraph"; @@ -1153,12 +1174,12 @@ Please read the section on [determinism](#determinism) for more details. const getTime = task("getTime", () => Date.now()); const myWorkflow = entrypoint( - { checkpointer: checkpointer }, + { checkpointer, name: "workflow" }, async (inputs: { t0: number }): Promise => { const t1 = await getTime(); - + const deltaT = t1 - inputs.t0; - + if (deltaT > 1000) { const result = await slowTask(1); const value = interrupt("question"); @@ -1171,4 +1192,4 @@ Please read the section on [determinism](#determinism) for more details. } ); ``` - ::: \ No newline at end of file + :::