Skip to main content
Version: 1.12.x

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_enabled setting 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:

    1. In the Langflow .env file, set the environment variable to true:
      LANGFLOW_DEVELOPER_API_ENABLED=true
    2. Start your Langflow server with the .env file enabled:
      uv run langflow run --env-file .env

    For more information about configuring environment variables, see Environment variables.

  • Create a Langflow API key

  • 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.

HeaderDescriptionExample
Content-TypeSpecifies the JSON format.application/json
x-api-keyYour Langflow API key.sk-...
acceptOptional. 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.

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): Langflow EventManager frames 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.

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.

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.

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 /run and /webhook endpoints if you are migrating from or comparing with the v1 API.

Was this page helpful?

Support
Search