Deploy to Azure¶
This guide walks you through deploying the simple_agent example to Azure Functions, step by step.
No Azure experience required — every command is explained and copy-paste ready.
Who this guide is for¶
You know Python and pip. You have cloned this repo and run the simple_agent example locally.
Now you want to deploy it to Azure so it runs in the cloud. This guide assumes you have never used Azure before.
What you are deploying¶
azure-functions-langgraph turns LangGraph agents into Azure Functions HTTP endpoints.
After deployment, your agent exposes:
- Native routes —
/api/health,/api/graphs/{name}/invoke,/api/graphs/{name}/stream - Platform-compatible routes (opt-in) —
/api/threads,/api/runs/wait,/api/runs/stream, and the full LangGraph Platform API surface
The simple_agent example is a two-node greeting graph (greet → farewell) that does not call any LLM.
If your own graph uses OpenAI or another provider, you will set those API keys in Step 8.
Azure concepts you need for this guide¶
New to Azure? Read Choose an Azure Functions Hosting Plan for a visual decision tree, plan comparison, and cost guidance.
| Term | What it means |
|---|---|
| Function App | Your deployed application. Like a Flask/FastAPI app running in the cloud. |
| Hosting plan | Controls how your app scales, how fast it responds, and how much it costs. |
| Resource Group | A folder for Azure resources. Delete it to clean up everything at once. |
| Storage Account | Required by Azure Functions for internal state. Also used by this package for checkpoints and thread metadata. |
Recommended plan for this repo¶
| Default plan | Premium (EP1) |
| Why | LangGraph agents often call LLM APIs that take several seconds to respond. Premium provides always-warm instances that eliminate cold starts, faster dependency builds during deployment, VNet support for private endpoints, and reliable Server-Sent Events (SSE) streaming — instances stay alive for the full response. |
| Switch to Flex Consumption if | Your graph is lightweight (no LLM calls), response times are predictable, and you want to minimize idle cost. The simple_agent example works fine on Flex Consumption. |
| Switch to Dedicated (B1) if | You need fixed monthly cost with no per-execution billing. |
⚠️ Premium plans have an always-on cost even when idle. See Choose an Azure Functions Hosting Plan for pricing details. For testing, Flex Consumption is cheaper.
Before you start¶
| Requirement | How to check | Install if missing |
|---|---|---|
| Azure account | portal.azure.com | Create free account |
| Azure CLI | az --version |
Install Azure CLI |
| Azure Functions Core Tools v4 | func --version |
Install Core Tools |
| Python 3.10–3.13 | python --version |
python.org |
jq (for JSON parsing in curl examples) |
jq --version |
stedolan.github.io/jq |
| Local example working | func start → curl /api/health returns OK |
See examples/simple_agent |
⚠️ Verify locally first. If your project doesn't work with
func start, it won't work on Azure.
Read these warnings before provisioning¶
- Storage account names must be globally unique across all of Azure. Use a name like
stlanggraph+ a random suffix. Only lowercase letters and numbers, 3–24 characters. - Use one region for all resources. Mixing regions adds latency and can cause failures.
- Local
.envvalues don't automatically appear on Azure. You must set app settings separately viaaz functionapp config appsettings set(see Step 8). - First deploy takes longer than expected. Azure runs a remote build to install your Python dependencies (including LangGraph, langchain-core, etc.). Wait for the "Deployment successful" message.
- Deleting local files does not delete Azure resources. You must explicitly delete the resource group to stop billing (see Clean up resources).
- LangGraph dependencies are large (~150+ MB). Remote build times can be 2–5 minutes on first deploy. Premium plans use express build which is faster.
- The
simple_agentexample does NOT need an LLM API key. It uses a hardcoded greeting graph. Only setOPENAI_API_KEYif your own graph actually calls an LLM provider. - Platform-compatible routes require Azure Storage backends. The threads API needs a blob container (checkpoints) and a table (thread metadata). These are created in Step 7.
Deploy simple_agent with platform-compatible routes¶
This example deploys with platform_compat=True, which enables the full threads/runs API alongside native routes. This requires Azure Blob Storage (checkpoints) and Azure Table Storage (thread metadata).
Step 1 — Copy the example project¶
Step 2 — Update requirements.txt¶
Replace the contents of requirements.txt with:
cat > requirements.txt << 'EOF'
azure-functions
azure-functions-langgraph[azure-blob,azure-table]
langgraph>=1.0,<2.0
langchain-core>=1.0,<2.0
EOF
The
[azure-blob,azure-table]extras installazure-storage-blobandazure-data-tablesfor persistent checkpointer and thread store.
Step 3 — Modify function_app.py for Azure¶
Replace function_app.py with the Azure-ready version that connects storage backends:
cat > function_app.py << 'PYEOF'
"""Simple agent — Azure Functions entry point (Azure deployment)."""
import os
from azure.storage.blob import BlobServiceClient
from graph import builder # Import the builder, NOT compiled_graph
from azure_functions_langgraph import LangGraphApp
from azure_functions_langgraph.checkpointers.azure_blob import AzureBlobCheckpointSaver
from azure_functions_langgraph.stores.azure_table import AzureTableThreadStore
# Storage backends
conn_str = os.environ["AZURE_STORAGE_CONNECTION_STRING"]
blob_service = BlobServiceClient.from_connection_string(conn_str)
container = blob_service.get_container_client("langgraph-checkpoints")
checkpointer = AzureBlobCheckpointSaver(container_client=container)
thread_store = AzureTableThreadStore.from_connection_string(
connection_string=conn_str,
table_name="langgraphthreads",
)
# Compile with checkpointer for persistent state
compiled_graph = builder.compile(checkpointer=checkpointer)
langgraph_app = LangGraphApp(platform_compat=True)
langgraph_app.thread_store = thread_store # Set via property, NOT register()
langgraph_app.register(
graph=compiled_graph,
name="simple_agent",
description="A simple two-node greeting agent",
)
app = langgraph_app.function_app
PYEOF
Key differences from the local version:
Local (function_app.py) |
Azure (function_app.py) |
|---|---|
from graph import compiled_graph |
from graph import builder — import the builder and compile with a checkpointer |
LangGraphApp() |
LangGraphApp(platform_compat=True) — enables threads/runs API |
| No storage | Blob checkpointer + Table thread store connected via AZURE_STORAGE_CONNECTION_STRING |
Step 4 — Verify locally¶
In another terminal:
Expected output:
{
"status": "ok",
"graphs": [
{
"name": "simple_agent",
"description": "A simple two-node greeting agent",
"has_checkpointer": true
}
]
}
⚠️ Platform routes (
/api/threads,/api/runs/...) won't work locally without Azure Storage. That's expected — they will work after deployment.
Stop the local server with Ctrl+C.
Step 5 — Sign in to Azure¶
How to find your subscription ID: Run
az account list --output tableand look for theSubscriptionIdcolumn.
Step 6 — Create Azure resources¶
RESOURCE_GROUP="rg-langgraph-agent"
LOCATION="koreacentral"
STORAGE_ACCOUNT="stlanggraph$(date +%s | tail -c 6)"
FUNCTIONAPP_NAME="func-langgraph-agent"
PLAN_NAME="plan-langgraph-agent"
Create the resource group:
Create a storage account:
az storage account create \
--name "$STORAGE_ACCOUNT" \
--resource-group "$RESOURCE_GROUP" \
--location "$LOCATION" \
--sku Standard_LRS
This takes about 10 seconds. Look for
"provisioningState": "Succeeded"in the output.
Create the Premium plan:
az functionapp plan create \
--name "$PLAN_NAME" \
--resource-group "$RESOURCE_GROUP" \
--location "$LOCATION" \
--sku EP1 \
--is-linux
Create the Function App on that plan:
az functionapp create \
--name "$FUNCTIONAPP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--storage-account "$STORAGE_ACCOUNT" \
--plan "$PLAN_NAME" \
--runtime python \
--runtime-version 3.11 \
--os-type Linux
Output includes:
Step 7 — Create storage backends for LangGraph¶
The platform-compatible routes need a blob container (for checkpoints) and a table (for thread metadata).
Get the storage connection string:
STORAGE_CONN_STR=$(az storage account show-connection-string \
--name "$STORAGE_ACCOUNT" \
--resource-group "$RESOURCE_GROUP" \
--query connectionString \
--output tsv)
Create the blob container:
az storage container create \
--name langgraph-checkpoints \
--connection-string "$STORAGE_CONN_STR"
Expected output:
Create the table:
Expected output:
Step 8 — Configure app settings¶
Set the storage connection string so function_app.py can read it at runtime:
az functionapp config appsettings set \
--name "$FUNCTIONAPP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--settings AZURE_STORAGE_CONNECTION_STRING="$STORAGE_CONN_STR"
If your graph uses an LLM provider (the simple_agent example does NOT):
# OpenAI
az functionapp config appsettings set \
--name "$FUNCTIONAPP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--settings OPENAI_API_KEY="sk-..."
# Or Azure OpenAI
az functionapp config appsettings set \
--name "$FUNCTIONAPP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--settings \
AZURE_OPENAI_API_KEY="..." \
AZURE_OPENAI_ENDPOINT="https://your-resource.openai.azure.com/" \
AZURE_OPENAI_DEPLOYMENT="gpt-4o"
Step 9 — Deploy the code¶
Output:
Getting site publishing info...
Starting the function app deployment...
Creating archive for current directory...
Performing remote build for functions project.
...
Deployment completed successfully.
Functions in func-langgraph-agent:
aflg_health - [httpTrigger]
aflg_simple_agent_invoke - [httpTrigger]
aflg_simple_agent_stream - [httpTrigger]
aflg_simple_agent_state - [httpTrigger]
aflg_platform_threads_create - [httpTrigger]
aflg_platform_threads_get - [httpTrigger]
aflg_platform_threads_update - [httpTrigger]
aflg_platform_threads_delete - [httpTrigger]
aflg_platform_threads_search - [httpTrigger]
aflg_platform_threads_count - [httpTrigger]
aflg_platform_threads_state_get - [httpTrigger]
aflg_platform_threads_state_update - [httpTrigger]
aflg_platform_threads_history - [httpTrigger]
aflg_platform_runs_wait - [httpTrigger]
aflg_platform_runs_stream - [httpTrigger]
aflg_platform_runs_wait_threadless - [httpTrigger]
aflg_platform_runs_stream_threadless - [httpTrigger]
aflg_platform_assistants_search - [httpTrigger]
aflg_platform_assistants_count - [httpTrigger]
aflg_platform_assistants_get - [httpTrigger]
Deployment successful.
⚠️ First deploy takes 2–5 minutes because of the large dependency tree (LangGraph, langchain-core, etc.). Premium plans use express build which is faster than Consumption.
Step 10 — Set the base URL¶
Step 11 — Verify native routes¶
Health check¶
Expected output:
{
"status": "ok",
"graphs": [
{
"name": "simple_agent",
"description": "A simple two-node greeting agent",
"has_checkpointer": true
}
]
}
Invoke the agent¶
curl -s -X POST "$BASE_URL/api/graphs/simple_agent/invoke" \
-H "Content-Type: application/json" \
-d '{
"input": {
"messages": [{"role": "human", "content": "World"}],
"greeting": ""
},
"config": {"configurable": {"thread_id": "native-001"}}
}' | jq .
Expected output:
{
"output": {
"messages": [
{"role": "human", "content": "World"},
{"role": "assistant", "content": "Hello, World! Goodbye!"}
],
"greeting": "Hello, World!"
}
}
Stream the agent¶
curl -s -X POST "$BASE_URL/api/graphs/simple_agent/stream" \
-H "Content-Type: application/json" \
-d '{
"input": {
"messages": [{"role": "human", "content": "World"}],
"greeting": ""
},
"config": {"configurable": {"thread_id": "native-002"}}
}'
Expected output (Server-Sent Events). The graph has two nodes (greet → farewell), so three state snapshots are emitted — initial input, after greet, after farewell:
event: data
data: {"messages":[{"role":"human","content":"World"}],"greeting":""}
event: data
data: {"messages":[{"role":"human","content":"World"}],"greeting":"Hello, World!"}
event: data
data: {"messages":[{"role":"human","content":"World"},{"role":"assistant","content":"Hello, World! Goodbye!"}],"greeting":"Hello, World!"}
event: end
data: {}
Step 12 — Verify platform-compatible routes¶
These routes follow the LangGraph Platform API specification.
Create a thread¶
THREAD_ID=$(curl -s -X POST "$BASE_URL/api/threads" \
-H "Content-Type: application/json" \
-d '{}' | jq -r '.thread_id')
echo "Thread ID: $THREAD_ID"
Expected output:
{
"thread_id": "550e8400-e29b-41d4-a716-446655440000",
"created_at": "2026-04-06T10:30:00Z",
"updated_at": "2026-04-06T10:30:00Z",
"metadata": null,
"status": "idle",
"values": null
}
Run the agent on the thread¶
curl -s -X POST "$BASE_URL/api/threads/$THREAD_ID/runs/wait" \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "simple_agent",
"input": {
"messages": [{"role": "human", "content": "World"}],
"greeting": ""
}
}' | jq .
Expected output:
{
"messages": [
{"role": "human", "content": "World"},
{"role": "assistant", "content": "Hello, World! Goodbye!"}
],
"greeting": "Hello, World!"
}
Stream the agent on the thread¶
curl -s -X POST "$BASE_URL/api/threads/$THREAD_ID/runs/stream" \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "simple_agent",
"input": {
"messages": [{"role": "human", "content": "World"}],
"greeting": ""
}
}'
Expected output:
event: metadata
data: {"run_id": "1f8a6f2d-3304-4556-89ce-37d4cabc1234"}
event: values
data: {"messages":[{"role":"human","content":"World"}],"greeting":""}
event: values
data: {"messages":[{"role":"human","content":"World"}],"greeting":"Hello, World!"}
event: values
data: {"messages":[{"role":"human","content":"World"},{"role":"assistant","content":"Hello, World! Goodbye!"}],"greeting":"Hello, World!"}
event: end
data: null
Get thread state¶
Expected output (truncated):
{
"values": {
"messages": [
{"role": "human", "content": "World"},
{"role": "assistant", "content": "Hello, World! Goodbye!"}
],
"greeting": "Hello, World!"
},
"next": [],
"checkpoint": {
"thread_id": "550e8400-...",
"checkpoint_ns": "",
"checkpoint_id": "1efc4c4f-..."
}
}
Search threads¶
curl -s -X POST "$BASE_URL/api/threads/search" \
-H "Content-Type: application/json" \
-d '{}' | jq .
Threadless run (no thread required)¶
curl -s -X POST "$BASE_URL/api/runs/wait" \
-H "Content-Type: application/json" \
-d '{
"assistant_id": "simple_agent",
"input": {
"messages": [{"role": "human", "content": "World"}],
"greeting": ""
}
}' | jq .
List registered assistants¶
curl -s -X POST "$BASE_URL/api/assistants/search" \
-H "Content-Type: application/json" \
-d '{}' | jq .
Expected output:
[
{
"assistant_id": "simple_agent",
"graph_id": "simple_agent",
"name": "simple_agent",
"description": "A simple two-node greeting agent"
}
]
Step 13 — Watch logs¶
You will see entries like:
Executing 'Functions.aflg_health' (Reason='...', Id=ab2a5eb3-...)
Executed 'Functions.aflg_health' (Succeeded, Duration=12ms)
Executing 'Functions.aflg_platform_runs_wait' (Reason='...', Id=4d5267c7-...)
Executed 'Functions.aflg_platform_runs_wait' (Succeeded, Duration=41ms)
Press Ctrl+C to stop the log stream.
If you need a different plan¶
The example above uses Premium (EP1). If you want a different plan, only the plan + Function App creation commands change. Everything else stays the same.
See Choose an Azure Functions Hosting Plan for complete per-plan commands.
Flex Consumption — for lowest cost¶
Replace Step 6 plan and Function App creation with:
# No separate plan needed — Flex Consumption is serverless
az functionapp create \
--name "$FUNCTIONAPP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--storage-account "$STORAGE_ACCOUNT" \
--flexconsumption-location "$LOCATION" \
--runtime python \
--runtime-version 3.11
⚠️ Timeout limit: Flex Consumption has a 30-minute function timeout (configurable via
functionTimeoutinhost.json), but cold starts can be slower than Premium. Streaming responses may be less reliable under high latency.
Dedicated (B1) — for fixed monthly cost¶
Replace Step 6 plan and Function App creation with:
# Create the App Service plan
az appservice plan create \
--name "$PLAN_NAME" \
--resource-group "$RESOURCE_GROUP" \
--location "$LOCATION" \
--sku B1 \
--is-linux
# Create the Function App on that plan
az functionapp create \
--name "$FUNCTIONAPP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--storage-account "$STORAGE_ACCOUNT" \
--plan "$PLAN_NAME" \
--runtime python \
--runtime-version 3.11 \
--os-type Linux
⚠️ Slow builds: B1 Dedicated plans have limited compute. First deployment with LangGraph dependencies can take 5+ minutes and may time out. If deployment fails, retry
func azure functionapp publish.
Troubleshooting¶
Provisioning failed¶
| Symptom | Usually means | How to fix |
|---|---|---|
StorageAccountAlreadyTaken |
Storage account name not globally unique | Add a random suffix: stlanggraph$(date +%s \| tail -c 6) |
LocationNotAvailableForResourceType |
Region doesn't support your plan | Use az functionapp list-flexconsumption-locations -o table or pick a major region (eastus, westeurope, koreacentral) |
SubscriptionNotFound |
Wrong subscription selected | Run az account list -o table and az account set --subscription <ID> |
SkuNotAvailable for EP1 |
Premium not available in your region | Try a different region or use Flex Consumption |
Deployment failed¶
| Symptom | Usually means | How to fix |
|---|---|---|
ModuleNotFoundError in build logs |
Missing or wrong requirements.txt |
Ensure azure-functions-langgraph[azure-blob,azure-table] is in requirements.txt |
| Build timeout (>10 minutes) | Large dependencies on slow plan | Use Premium (faster express build) or retry |
Can't find app with name |
Function App not fully provisioned | Wait 30 seconds and retry |
ImportError: azure.storage.blob |
Missing extras in requirements | Use azure-functions-langgraph[azure-blob,azure-table], not just azure-functions-langgraph |
The app deployed but does not behave correctly¶
| Symptom | Usually means | How to fix |
|---|---|---|
/api/health returns 404 |
Functions not registered | Check func azure functionapp publish output — should list aflg_health |
/api/health returns 500 |
Missing AZURE_STORAGE_CONNECTION_STRING |
Set it: az functionapp config appsettings set --settings AZURE_STORAGE_CONNECTION_STRING="..." |
/api/threads returns 500 |
Blob container or table doesn't exist | Run Step 7 commands |
Invoke returns KeyError: OPENAI_API_KEY |
Your graph expects an LLM key | Set it via az functionapp config appsettings set --settings OPENAI_API_KEY="sk-..." |
| Invoke returns correct data but streaming hangs | SSE connection interrupted | Check timeout settings; Premium is more reliable for streaming |
| Thread state is empty after invoke | Using compiled_graph instead of builder |
Import builder from graph.py and compile with checkpointer (see Step 3) |
Logs and monitoring¶
# Live log stream (real-time)
func azure functionapp logstream "$FUNCTIONAPP_NAME"
# Recent invocations via Application Insights
az monitor app-insights query \
--app "$FUNCTIONAPP_NAME" \
--analytics-query "requests | where timestamp > ago(30m) | project timestamp, name, resultCode, duration | order by timestamp desc | take 10"
Before opening an issue¶
If you're stuck, please include the following when opening a GitHub issue:
# 1. Azure CLI version
az --version
# 2. Functions Core Tools version
func --version
# 3. Python version
python --version
# 4. Package version
pip show azure-functions-langgraph
# 5. Function App status
az functionapp show \
--name "$FUNCTIONAPP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "{state:state, runtime:siteConfig.linuxFxVersion}"
# 6. App settings (secrets will be redacted)
az functionapp config appsettings list \
--name "$FUNCTIONAPP_NAME" \
--resource-group "$RESOURCE_GROUP" \
--query "[].name"
# 7. Recent logs
func azure functionapp logstream "$FUNCTIONAPP_NAME"
Clean up resources¶
⚠️ Premium plans cost money even when idle. Always clean up after testing.
The --no-wait flag returns immediately. Deletion happens in the background and takes 1–2 minutes.
To verify deletion:
Sources¶
- Azure Functions Python quickstart — Official getting-started guide
- Azure Functions Core Tools reference — CLI command reference
- Azure Functions app settings — Environment variables and configuration
- Azure Functions hosting plans — Plan comparison and limits
- Premium plan — Premium plan details
- Azure Blob Storage documentation — Blob storage for checkpoints
- Azure Table Storage documentation — Table storage for thread metadata
- Functions monitoring and telemetry — Application Insights and monitoring
See Also¶
- Choose an Azure Functions Hosting Plan — Plan selection guide with decision tree
production-guide.md— Auth, observability, and production hardeningconfiguration.md— Configuration referencearchitecture.md— Package architecturetroubleshooting.md— Extended troubleshooting guideazure-functions-scaffoldazure-functions-validationazure-functions-openapiazure-functions-loggingazure-functions-doctor