Workflow API quickstart
The Workflow API executes Langflow flows over POST /api/v2/workflows in three modes.
Use the default sync mode when you want to wait for the full JSON result in one response. Use stream when you need live Server-Sent Events (SSE) emitted as the flow runs. Use background when you want to queue a job and then poll or re-attach to its event stream.
This quickstart runs the same flow with each mode, so you can use the mode that fits your application.
Prerequisites
-
Install and start Langflow with the developer API enabled
The Workflow API endpoints require the
developer_api_enabledsetting to be enabled. If this setting is disabled, these endpoints will return a 404 Not Found error.To enable the developer API endpoint, do the following:
- In the Langflow
.envfile, set the environment variable totrue:LANGFLOW_DEVELOPER_API_ENABLED=true - Start your Langflow server with the
.envfile enabled:uv run langflow run --env-file .env
For more information about configuring environment variables, see Environment variables.
- In the Langflow
-
Create a flow that you want to execute
-
Get the flow ID or endpoint name of the flow you want to execute
Set environment variables
All code examples in this documentation assume you have set the following environment variables:
Python:
import os
LANGFLOW_SERVER_URL = os.getenv("LANGFLOW_SERVER_URL")
LANGFLOW_API_KEY = os.getenv("LANGFLOW_API_KEY")
TypeScript/JavaScript:
const LANGFLOW_SERVER_URL = process.env.LANGFLOW_SERVER_URL;
const LANGFLOW_API_KEY = process.env.LANGFLOW_API_KEY;
Set these environment variables before running the examples, or replace the variable references in the code examples with your actual Langflow server URL and API key.
The default LANGFLOW_SERVER_URL for a local Langflow deployment is http://localhost:7860.
For remote deployments, the domain is set by your hosting service, such as https://UUID.ngrok.app.
Authentication and headers
All Workflow API requests require authentication using a Langflow API key. The API key is passed in the x-api-key header.
For more information, see Create a Langflow API key.
| Header | Description | Example |
|---|---|---|
Content-Type | Specifies the JSON format. | application/json |
x-api-key | Your Langflow API key. | sk-... |
accept | Optional. Specifies the response format. | application/json |
The examples on this page also expect FLOW_ID to be set to your flow's UUID. Use a chat-style flow (for example Basic Prompting) so output.text contains the assistant reply.
Run a flow synchronously
Omit mode or set "mode": "sync" to run inline. When the flow finishes, the response includes a top-level output.text field with the primary answer.
- Python
- JavaScript
- curl
import os
import requests
base = os.environ.get("LANGFLOW_URL") or os.environ.get("LANGFLOW_SERVER_URL", "")
flow_id = os.environ.get("FLOW_ID", "")
api_key = os.environ.get("LANGFLOW_API_KEY", "")
headers = {"Content-Type": "application/json", "x-api-key": api_key}
payload = {
"flow_id": flow_id,
"input_value": "what is 2+2",
"session_id": "session-123",
"mode": "sync",
}
response = requests.post(f"{base}/api/v2/workflows", headers=headers, json=payload, timeout=120)
response.raise_for_status()
body = response.json()
print(body["output"]["text"])
(async () => {
const base = (process.env.LANGFLOW_URL ?? process.env.LANGFLOW_SERVER_URL ?? "").replace(/\/$/, "");
const flowId = process.env.FLOW_ID ?? "";
const apiKey = process.env.LANGFLOW_API_KEY ?? "";
const response = await fetch(`${base}/api/v2/workflows`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
flow_id: flowId,
input_value: "what is 2+2",
session_id: "session-123",
mode: "sync",
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const body = await response.json();
console.log(body.output.text);
})().catch((error) => {
console.error(error);
process.exit(1);
});
curl -X POST \
"$LANGFLOW_SERVER_URL/api/v2/workflows" \
-H "Content-Type: application/json" \
-H "x-api-key: $LANGFLOW_API_KEY" \
-d '{
"flow_id": "'"${FLOW_ID}"'",
"input_value": "what is 2+2",
"session_id": "session-123",
"mode": "sync"
}'
Example sync response
{
"flow_id": "67ccd2be-17f0-8190-81ff-3bb2cf6508e6",
"session_id": "session-123",
"object": "response",
"status": "completed",
"output": {
"reason": "single",
"text": "2 + 2 equals 4.",
"source": "ChatOutput-xyz"
},
"has_errors": false
}
Pass the same session_id on the next request to continue chat history for that conversation.
Stream LLM tokens
Set "mode": "stream" to open an SSE stream. Choose the wire format with stream_protocol:
langflow(default): LangflowEventManagerframes for existing Langflow clients.agui: AG-UI lifecycle events for compatible clients.
For request examples, event types, and protocol details, see Execute flow with selected stream mode in the Workflow API reference.
Langflow EventManager (default)
Each data: line is JSON shaped like {"event": "<type>", "data": {...}}. The example below prints only token events (data.chunk) as they arrive.
- Python
- JavaScript
- curl
import json
import os
import requests
base = os.environ.get("LANGFLOW_URL") or os.environ.get("LANGFLOW_SERVER_URL", "")
flow_id = os.environ.get("FLOW_ID", "")
api_key = os.environ.get("LANGFLOW_API_KEY", "")
headers = {"Content-Type": "application/json", "x-api-key": api_key}
payload = {
"flow_id": flow_id,
"input_value": "Tell me a short joke.",
"mode": "stream",
"session_id": "session-123",
}
with requests.post(
f"{base}/api/v2/workflows",
headers=headers,
json=payload,
stream=True,
timeout=120,
) as response:
response.raise_for_status()
for line in response.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
frame = json.loads(line.removeprefix("data:").strip())
if frame.get("event") == "token":
print(frame["data"]["chunk"], end="", flush=True)
print()
(async () => {
const base = (process.env.LANGFLOW_URL ?? process.env.LANGFLOW_SERVER_URL ?? "").replace(/\/$/, "");
const flowId = process.env.FLOW_ID ?? "";
const apiKey = process.env.LANGFLOW_API_KEY ?? "";
const response = await fetch(`${base}/api/v2/workflows`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
},
body: JSON.stringify({
flow_id: flowId,
input_value: "Tell me a short joke.",
mode: "stream",
session_id: "session-123",
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data:")) {
continue;
}
const frame = JSON.parse(line.slice(5).trim());
if (frame.event === "token") {
process.stdout.write(frame.data.chunk);
}
}
}
buffer += decoder.decode();
if (buffer.trim().startsWith("data:")) {
const frame = JSON.parse(buffer.slice(5).trim());
if (frame.event === "token") {
process.stdout.write(frame.data.chunk);
}
}
process.stdout.write("\n");
})().catch((error) => {
console.error(error);
process.exit(1);
});
curl -N -X POST \
"$LANGFLOW_SERVER_URL/api/v2/workflows" \
-H "Content-Type: application/json" \
-H "x-api-key: $LANGFLOW_API_KEY" \
-d '{
"flow_id": "'"${FLOW_ID}"'",
"input_value": "Tell me a short joke.",
"mode": "stream",
"session_id": "session-123"
}'
AG-UI
Send "stream_protocol": "agui". Each data: line contains one AG-UI event as JSON. The example below handles TEXT_MESSAGE_CONTENT, TOOL_CALL_RESULT, and RUN_FINISHED, extracts a number from the first reply, then chains a follow-up prompt in the same session_id.
- Python
- JavaScript
- curl
import json
import os
import re
import requests
base = os.environ.get("LANGFLOW_URL") or os.environ.get("LANGFLOW_SERVER_URL", "")
flow_id = os.environ.get("FLOW_ID", "67ccd2be-17f0-8190-81ff-3bb2cf6508e6")
api_key = os.environ.get("LANGFLOW_API_KEY", "")
session_id = os.environ.get("AGUI_SESSION_ID", "thread-123")
number = re.compile(r"-?\d+(?:\.\d+)?")
def extract_number(text: str, tool_results: list) -> float | None:
"""Prefer calculator tool output, then the last number in the assistant reply."""
for raw in reversed(tool_results):
if matches := number.findall(str(raw)):
return float(matches[-1])
if matches := number.findall(text):
return float(matches[-1])
return None
def ask(prompt: str) -> tuple[str, list]:
"""Stream one AG-UI run; return assistant text and tool results."""
text = ""
tool_results = []
with requests.post(
f"{base}/api/v2/workflows",
headers={
"Content-Type": "application/json",
"x-api-key": api_key,
"Accept": "text/event-stream",
},
json={
"flow_id": flow_id,
"input_value": prompt,
"mode": "stream",
"stream_protocol": "agui",
"session_id": session_id,
},
stream=True,
timeout=120,
) as response:
response.raise_for_status()
for line in response.iter_lines(decode_unicode=True):
if not line or not line.startswith("data:"):
continue
event = json.loads(line.removeprefix("data:").strip())
match event.get("type"):
case "TEXT_MESSAGE_CONTENT":
text += event.get("delta", "")
case "TOOL_CALL_RESULT":
tool_results.append(event.get("content") or event.get("result"))
case "RUN_ERROR":
raise RuntimeError(event.get("message") or "Run failed")
case "RUN_FINISHED":
break
return text, tool_results
prompt1 = os.environ.get("AGUI_PROMPT1", "What is 847 divided by 7?")
multiplier = int(os.environ.get("AGUI_MULTIPLIER", "3"))
print(f"User: {prompt1}")
reply1, tools1 = ask(prompt1)
quotient = extract_number(reply1, tools1)
if quotient is None:
raise SystemExit("Could not extract a number from run 1.")
print(f"Assistant: {reply1.strip()}")
print(f"Extracted: {quotient:g}")
prompt2 = os.environ.get("AGUI_PROMPT2") or f"Now multiply {quotient:g} by {multiplier}."
print(f"\nUser: {prompt2}")
reply2, tools2 = ask(prompt2)
product = extract_number(reply2, tools2)
if product is None:
raise SystemExit("Could not extract a number from run 2.")
print(f"Assistant: {reply2.strip()}")
print("\n=== Calculation chain ===")
print(f"847 ÷ 7 = {quotient:g}")
print(f"{quotient:g} × {multiplier} = {product:g}")
const base = (process.env.LANGFLOW_URL ?? process.env.LANGFLOW_SERVER_URL ?? "").replace(/\/$/, "");
const flowId = process.env.FLOW_ID ?? "67ccd2be-17f0-8190-81ff-3bb2cf6508e6";
const apiKey = process.env.LANGFLOW_API_KEY ?? "";
const sessionId = process.env.AGUI_SESSION_ID ?? "thread-123";
const NUMBER = /-?\d+(?:\.\d+)?/;
function numbersFromToolResult(raw) {
if (raw == null) {
return [];
}
if (typeof raw === "number") {
return [String(raw)];
}
if (typeof raw === "string") {
return raw.match(new RegExp(NUMBER.source, "g")) ?? [];
}
if (typeof raw === "object") {
for (const key of ["content", "result", "output", "text"]) {
const value = raw[key];
if (value == null) {
continue;
}
if (typeof value === "number") {
return [String(value)];
}
const asText = typeof value === "string" ? value : JSON.stringify(value);
const matches = asText.match(new RegExp(NUMBER.source, "g"));
if (matches?.length) {
return matches;
}
}
const serialized = JSON.stringify(raw);
return serialized.match(new RegExp(NUMBER.source, "g")) ?? [];
}
return String(raw).match(new RegExp(NUMBER.source, "g")) ?? [];
}
function extractNumber(text, toolResults) {
for (let i = toolResults.length - 1; i >= 0; i -= 1) {
const matches = numbersFromToolResult(toolResults[i]);
if (matches.length) {
return Number(matches[matches.length - 1]);
}
}
const matches = text.match(new RegExp(NUMBER.source, "g"));
return matches?.length ? Number(matches[matches.length - 1]) : null;
}
async function ask(prompt) {
let text = "";
const toolResults = [];
const response = await fetch(`${base}/api/v2/workflows`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": apiKey,
Accept: "text/event-stream",
},
body: JSON.stringify({
flow_id: flowId,
input_value: prompt,
mode: "stream",
stream_protocol: "agui",
session_id: sessionId,
}),
});
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${await response.text()}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() ?? "";
for (const line of lines) {
if (!line.startsWith("data:")) {
continue;
}
const event = JSON.parse(line.slice(5).trim());
switch (event.type) {
case "TEXT_MESSAGE_CONTENT":
text += event.delta ?? "";
break;
case "TOOL_CALL_RESULT":
toolResults.push(event.content ?? event.result);
break;
case "RUN_ERROR":
throw new Error(event.message ?? "Run failed");
case "RUN_FINISHED":
await reader.cancel();
return { text, toolResults };
default:
break;
}
}
}
return { text, toolResults };
}
async function main() {
const prompt1 = process.env.AGUI_PROMPT1 ?? "What is 847 divided by 7?";
const multiplier = Number(process.env.AGUI_MULTIPLIER ?? "3");
console.log(`User: ${prompt1}`);
const { text: reply1, toolResults: tools1 } = await ask(prompt1);
const quotient = extractNumber(reply1, tools1);
if (quotient == null) {
throw new Error("Could not extract a number from run 1.");
}
console.log(`Assistant: ${reply1.trim()}`);
console.log(`Extracted: ${quotient}`);
const prompt2 = process.env.AGUI_PROMPT2 ?? `Now multiply ${quotient} by ${multiplier}.`;
console.log(`\nUser: ${prompt2}`);
const { text: reply2, toolResults: tools2 } = await ask(prompt2);
const product = extractNumber(reply2, tools2);
if (product == null) {
throw new Error("Could not extract a number from run 2.");
}
console.log(`Assistant: ${reply2.trim()}`);
console.log("\n=== Calculation chain ===");
console.log(`847 ÷ 7 = ${quotient}`);
console.log(`${quotient} × ${multiplier} = ${product}`);
}
main().catch((error) => {
console.error(error);
process.exit(1);
});
#!/usr/bin/env bash
# Parse AG-UI SSE frames with jq and chain two workflow runs in one session.
set -euo pipefail
BASE="${LANGFLOW_URL:-${LANGFLOW_SERVER_URL:-}}"
FLOW_ID="${FLOW_ID:-67ccd2be-17f0-8190-81ff-3bb2cf6508e6}"
API_KEY="${LANGFLOW_API_KEY:-}"
SESSION_ID="${AGUI_SESSION_ID:-thread-123}"
MULTIPLIER="${AGUI_MULTIPLIER:-3}"
PROMPT1="${AGUI_PROMPT1:-What is 847 divided by 7?}"
TEXT_FILE="$(mktemp)"
TOOLS_FILE="$(mktemp)"
trap 'rm -f "$TEXT_FILE" "$TOOLS_FILE"' EXIT
extract_number() {
local text last=""
text="$(cat "$TEXT_FILE")"
while IFS= read -r line; do
match="$(grep -oE -- '-?[0-9]+(\.[0-9]+)?' <<< "$line" | tail -n1 || true)"
if [[ -n "$match" ]]; then
echo "$match"
return 0
fi
done < <(tac "$TOOLS_FILE" 2>/dev/null || tail -r "$TOOLS_FILE")
while read -r num; do
last="$num"
done < <(grep -oE -- '-?[0-9]+(\.[0-9]+)?' <<< "$text" || true)
if [[ -n "$last" ]]; then
echo "$last"
fi
}
ask() {
local prompt="$1"
: > "$TEXT_FILE"
: > "$TOOLS_FILE"
curl --fail-with-body -N -sS -X POST "${BASE}/api/v2/workflows" \
-H "Content-Type: application/json" \
-H "x-api-key: ${API_KEY}" \
-d "$(jq -n \
--arg flow_id "$FLOW_ID" \
--arg input_value "$prompt" \
--arg session_id "$SESSION_ID" \
'{flow_id: $flow_id, input_value: $input_value, mode: "stream", stream_protocol: "agui", session_id: $session_id}')" \
| while IFS= read -r line; do
[[ "$line" == data:* ]] || continue
json="${line#data: }"
case "$(jq -r '.type' <<<"$json")" in
TEXT_MESSAGE_CONTENT)
jq -r '.delta // ""' <<<"$json" >> "$TEXT_FILE"
;;
TOOL_CALL_RESULT)
jq -r '.content // .result // ""' <<<"$json" >> "$TOOLS_FILE"
;;
RUN_ERROR)
jq -r '.message // "Run failed"' <<<"$json" >&2
exit 1
;;
RUN_FINISHED)
break
;;
esac
done
}
echo "User: ${PROMPT1}"
ask "$PROMPT1"
QUOTIENT="$(extract_number)"
if [[ -z "${QUOTIENT:-}" ]]; then
echo "Could not extract a number from run 1." >&2
exit 1
fi
echo "Assistant: $(tr -d '\n' < "$TEXT_FILE")"
echo "Extracted: ${QUOTIENT}"
PROMPT2="${AGUI_PROMPT2:-Now multiply ${QUOTIENT} by ${MULTIPLIER}.}"
echo
echo "User: ${PROMPT2}"
ask "$PROMPT2"
PRODUCT="$(extract_number)"
if [[ -z "${PRODUCT:-}" ]]; then
echo "Could not extract a number from run 2." >&2
exit 1
fi
echo "Assistant: $(tr -d '\n' < "$TEXT_FILE")"
echo
echo "=== Calculation chain ==="
echo "847 ÷ 7 = ${QUOTIENT}"
echo "${QUOTIENT} × ${MULTIPLIER} = ${PRODUCT}"
Run in the background
Set "mode": "background" to queue a job and receive a job_id immediately. Poll GET /api/v2/workflows?job_id=… until status is completed, then read output.text from the result.
- Python
- JavaScript
- curl
import os
import time
import requests
base = os.environ.get("LANGFLOW_URL") or os.environ.get("LANGFLOW_SERVER_URL", "")
flow_id = os.environ.get("FLOW_ID", "")
api_key = os.environ.get("LANGFLOW_API_KEY", "")
headers = {"Content-Type": "application/json", "x-api-key": api_key}
start = requests.post(
f"{base}/api/v2/workflows",
headers=headers,
json={
"flow_id": flow_id,
"input_value": "Process this in the background",
"session_id": "session-456",
"mode": "background",
},
timeout=60,
)
start.raise_for_status()
job_id = start.json()["job_id"]
print(f"Queued job {job_id}")
while True:
status = requests.get(
f"{base}/api/v2/workflows",
headers=headers,
params={"job_id": job_id},
timeout=60,
)
status.raise_for_status()
body = status.json()
if body.get("object") == "response" and body.get("status") == "completed":
print(body["output"]["text"])
break
if body.get("status") in {"failed", "cancelled", "timed_out"}:
raise SystemExit(f"Job ended with status {body.get('status')}")
time.sleep(1)
(async () => {
const base = (process.env.LANGFLOW_URL ?? process.env.LANGFLOW_SERVER_URL ?? "").replace(/\/$/, "");
const flowId = process.env.FLOW_ID ?? "";
const apiKey = process.env.LANGFLOW_API_KEY ?? "";
const headers = {
"Content-Type": "application/json",
"x-api-key": apiKey,
};
const start = await fetch(`${base}/api/v2/workflows`, {
method: "POST",
headers,
body: JSON.stringify({
flow_id: flowId,
input_value: "Process this in the background",
session_id: "session-456",
mode: "background",
}),
});
if (!start.ok) {
throw new Error(`HTTP ${start.status}: ${await start.text()}`);
}
const { job_id: jobId } = await start.json();
console.log(`Queued job ${jobId}`);
while (true) {
const status = await fetch(`${base}/api/v2/workflows?job_id=${encodeURIComponent(jobId)}`, {
headers,
});
if (!status.ok) {
throw new Error(`HTTP ${status.status}: ${await status.text()}`);
}
const body = await status.json();
if (body.object === "response" && body.status === "completed") {
console.log(body.output.text);
break;
}
if (["failed", "cancelled", "timed_out"].includes(body.status)) {
throw new Error(`Job ended with status ${body.status}`);
}
await new Promise((resolve) => setTimeout(resolve, 1000));
}
})().catch((error) => {
console.error(error);
process.exit(1);
});
#!/usr/bin/env bash
set -euo pipefail
JOB_ID="$(
curl --fail-with-body -sS -X POST \
"$LANGFLOW_SERVER_URL/api/v2/workflows" \
-H "Content-Type: application/json" \
-H "x-api-key: $LANGFLOW_API_KEY" \
-d '{
"flow_id": "'"${FLOW_ID}"'",
"input_value": "Process this in the background",
"session_id": "session-456",
"mode": "background"
}' | python3 -c 'import json,sys; print(json.load(sys.stdin)["job_id"])'
)"
echo "Queued job $JOB_ID"
while true; do
BODY="$(curl --fail-with-body -sS \
"$LANGFLOW_SERVER_URL/api/v2/workflows?job_id=$JOB_ID" \
-H "x-api-key: $LANGFLOW_API_KEY")"
STATUS="$(python3 -c 'import json,sys; print(json.load(sys.stdin).get("status",""))' <<<"$BODY")"
OBJECT="$(python3 -c 'import json,sys; print(json.load(sys.stdin).get("object",""))' <<<"$BODY")"
if [[ "$OBJECT" == "response" && "$STATUS" == "completed" ]]; then
python3 -c 'import json,sys; print(json.load(sys.stdin)["output"]["text"])' <<<"$BODY"
break
fi
if [[ "$STATUS" == "failed" || "$STATUS" == "cancelled" || "$STATUS" == "timed_out" ]]; then
echo "Job ended with status $STATUS" >&2
exit 1
fi
sleep 1
done
While a job is still queued or in_progress, the poll endpoint returns a job object (object: "job") with links for status, events, and stop. When the job completes, the same endpoint returns the full execution response.
To connect to a background run, see Re-attach to a background run.
Next steps
- Workflow API reference — request body fields, response schemas, status polling, stop endpoint, tweaks, globals, and error codes.
- Flow trigger endpoints — v1
/runand/webhookendpoints if you are migrating from or comparing with the v1 API.
Was this page helpful?