🦌 DeerFlow - 2.0
English | 中文 | 日本語 | Français | Русский | Português | 한국어
On February 28th, 2026, DeerFlow claimed the 🏆 #1 spot on GitHub Trending following the launch of version 2. Thanks a million to our incredible community — you made this happen! 💪🔥
DeerFlow (Deep Exploration and Efficient Research Flow) is an open-source super agent harness that orchestrates sub-agents, memory, and sandboxes to do almost anything — powered by extensible skills.
https://github.com/user-attachments/assets/a8bcadc4-e040-4cf2-8fda-dd768b999c18
[!NOTE] DeerFlow 2.0 is a ground-up rewrite. It shares no code with v1. If you're looking for the original Deep Research framework, it's maintained on the
1.xbranch — contributions there are still welcome. Active development has moved to 2.0.
Official Website
Learn more and see real demos on our official website. The landing-page case studies open as allowlisted, read-only showcases without requiring a sign-in.
Sister Projects
- LLM Space - Meet our secret weapon behind DeerFlow — one desktop tool to prototype agent ideas, inspect each harness step, replay failures, and benchmark performance.
Coding Plan from ByteDance Volcengine
- We strongly recommend using Doubao-Seed-2.0-Code, DeepSeek v3.2 and Kimi 2.5 to run DeerFlow
- Learn more
- 中国大陆地区的开发者请点击这里
InfoQuest
InfoQuest reader, web search, and image search use a 30-second HTTP connect/read
inactivity timeout. The crawl timeout and navigation_timeout settings remain
separate server-side options; they do not control the local HTTP timeout.
DeerFlow has newly integrated the intelligent search and crawling toolset independently developed by BytePlus — InfoQuest (supports free online experience)
Table of Contents
- 🦌 DeerFlow - 2.0
One-Line Agent Setup
If you use Claude Code, Codex, Cursor, Windsurf, or another coding agent, you can hand it the setup instructions in one sentence:
Help me clone DeerFlow if needed, then bootstrap it for local development by following https://raw.githubusercontent.com/bytedance/deer-flow/main/Install.md
That prompt is intended for coding agents. It tells the agent to clone the repo if needed, choose Docker when available, and stop with the exact next command plus any missing config the user still needs to provide.
Quick Start
Configuration
Application SQL JSON writes normalize non-finite floats (NaN, Infinity, -Infinity) to null on SQLite and PostgreSQL, including DB-backed agent definitions and skill-mutation records. Existing rows are not rewritten.
With run_events.backend: db, caller-supplied ISO 8601 event timestamps are normalized to UTC before storage, preserving their instant when SQLite drops timezone information. Timestamps without an offset are treated as UTC. Previously stored timestamps are not rewritten.
Run event streams include run.terminal for the owning worker's confirmed final
Run status, while run.end continues to describe each root graph completion.
Terminal publication is best effort; event persistence failure does not undo the
Run outcome or prevent stream completion. Recovery with built-in SQL Run and Event stores
sharing an engine uses one pooled connection for each guarded event write, so it
can publish with only one pool slot available. See Run Event Stream.
PostgreSQL async ORM connections and the offline command accept postgres://
and postgresql:// URLs, including ?sslmode=disable or ?sslmode=verify-full.
SSL modes are preserved; conflicting ssl/sslmode settings are rejected.
Explicit channel_binding options are rejected because asyncpg cannot enforce
them. Synchronous psycopg and LangGraph DSNs retain their libpq options.
With a fencing-capable SQL RunStore and DbRunEventStore, host-authorized
producer writes validate the persisted Run incarnation, producer epoch and state
even with run_events.producer_fencing: "off". Stale Journal flushes after
takeover or deletion may therefore be rejected in the default configuration.
On PostgreSQL, these writes hold the Thread Fence and RunRow lock through commit.
Checkpoint and branch history imports use a separate host grant bound to a
durable thread-operation reservation and the captured thread identity. Strict
mode permits these imports without treating synthetic turn IDs as admitted Runs;
deletion, reservation revocation or lease expiry invalidates their grant.
Messages preceding the first human input retain their existing synthetic turn 0.
On PostgreSQL, run_events.producer_fencing: "strict" additionally enables
database-level authorization checks against raw SQL and unaware legacy writers
after an offline cutover. The "off" setting keeps application-level producer
validation active. Before activation, the database INSERT trigger skips its
Thread/RunRow locks and retains a shared cutover lock until each INSERT
transaction ends. This does not remove the application locks above. Off-mode
event INSERTs require READ COMMITTED (also accepting PostgreSQL's READ UNCOMMITTED);
REPEATABLE READ and SERIALIZABLE are rejected to prevent stale policy snapshots
from bypassing activation. A write that loses a race with activation rolls back
with SQLSTATE 40001; retry the entire transaction with strict producer authority.
Activation requires stopping every Gateway and using the offline cutover command;
all restarted Gateways must use the same cutover ID. This does not recover missing
terminal events. To activate:
-
Deploy this version everywhere with
run_events.producer_fencing: "off". Complete migrations 0049 and 0050 and recover previously active Runs, then drain admissions and completion hooks. A terminal Run status alone does not prove its producer has drained. -
Stop every Gateway, Worker and other database client, including checkpointer pools and monitoring clients, and keep them stopped during activation.
-
From the repository root, using the installed backend environment, run:
uv run --no-sync --project backend python backend/scripts/run_producer_fencing.py \ --config config.yaml --cutover-id deployment-2026-10-27 --all-gateways-stoppedThe command refuses activation if other database sessions or pending/running Runs remain. Use a deployment ID that every Gateway will share.
-
Set
database.backend: postgres,run_events.backend: db,run_events.producer_fencing: strictandrun_events.producer_cutover_id: deployment-2026-10-27everywhere, then restart. Verify a new Run persists its initial event, Journal/Hook, delivery and terminal events and closes normally; unaware old writers must receive a database rejection.
Activation is durable: missing or mismatched cutover IDs and downgrades to "off"
or unsupported stores fail startup. This release requires controlled downtime;
it does not support a mixed-version rolling cutover. A crashed producer may stay
open until explicitly closed or revoked by authorized takeover/deletion; no
producer-expiry or terminal-reconciliation job is introduced.
Scheduled-task orphan recovery carries the captured Run incarnation and retention revision when publishing delivery and terminal evidence, including in strict mode. Unconfirmed publication is logged separately from the committed takeover; it does not undo the recovered Run or queue a new publication attempt.
Operators can extend lead-agent, subagent, and DeerMem extraction prompts with literal prepend/append configuration without editing source templates. See prompt overlays.
Optional per-model request_admission
paces requests to help stay within provider request-per-minute limits.
It is disabled by default; see the linked guide to enable it.
Enabling it disables exposed SDK retries and the Claude and Codex adapters'
internal retry loops so middleware retries pass through request admission again.
Middleware retries also cover HTTP 529 overload responses.
A warning identifies retry_max_attempts values overridden by admission.
For Google's official Gemini OpenAI-compatible endpoint, use the Gemini reasoning profile.
For MindIE XML tool calls, see the
argument parsing and newline compatibility guide.
Tool-result recovery treats null and empty call IDs alike — including the
"None" and "missing_tool_call_id" spellings middleware producers use when a
provider call had no ID — preserving middleware guidance when a result can be
uniquely paired; ambiguous partial results remain rejected.
Both synchronous and asynchronous streams retain this compatibility: tool-enabled
streams simulate chunks from a non-streaming response, while no-tool streams stay native.
Newline compatibility preserves literal escapes inside backtick and tilde code
fences, including unclosed fences, in complete and streamed replies.
-
Clone the DeerFlow repository
git clone https://github.com/bytedance/deer-flow.git cd deer-flow -
Run the setup wizard
From the project root directory (
deer-flow/), run:make setupThis launches an interactive wizard that guides you through choosing an LLM provider, optional web search, and execution/safety preferences such as sandbox mode, bash access, and file-write tools. It generates a minimal
config.yamland writes your keys to.env. Takes about 2 minutes.The wizard also lets you configure an optional web search provider, or skip it for now.
Brave web search preserves valid entries in mixed result lists. Malformed response containers or lists containing no usable entries return a structured format error; missing, null, or empty results keep the existing "No results found" response. Format errors also log the malformed container's path and type, or the absence of usable result objects, without including search queries, credentials, or payload values.
Several providers, including You.com, GroundRoute, Webz, InfoQuest and Jsonify, initialize HTTPX clients in a worker thread so synchronous TLS trust loading does not block the agent event loop; requests and session cleanup remain asynchronous.
GroundRoute web search and fetch contain malformed HTTP-success payloads the same way: a non-object payload, a
resultscontainer that is not a list, or a non-empty list with no object entries returns the provider's format error, while non-object entries in an otherwise valid list are skipped in order. Missing, null, or empty results keep the existing "No results found" response.DuckDuckGo web search distinguishes execution failures from empty results: rate limits, timeouts, blocked requests, client-setup failures, SDK errors, and a missing
ddgsdependency return a structuredDuckDuckGo search failed: ...error instead of "No results found", while a genuinely empty result set keeps the existing "No results found" response. The structured error text lets the harness classify an outage as transient or a configuration problem instead of a query that matched nothing.Tavily web search and fetch contain malformed HTTP-success payloads the same way: a non-object payload, or a
results/failed_resultscontainer that is not a list, returns the provider's format error, and non-object entries in an otherwise valid list are skipped in order. Missing, null, or empty results keep the existing response ([]for search,Error: No results foundfor fetch); missing or null search fields (title, , ) normalize to empty text, and a non-string fetch is coerced to text before the truncation limit.
For an explicit backend dotenv file, export DEER_FLOW_ENV_FILE before startup,
alongside DEER_FLOW_CONFIG_PATH if needed. For example, from backend/:
DEER_FLOW_ENV_FILE=/srv/deer-flow/stage.env DEER_FLOW_CONFIG_PATH=/srv/deer-flow/stage.yaml make gateway
Relative dotenv paths use the backend process working directory; absolute paths
work regardless of that directory. Existing process variables win. An unset
selector preserves default dotenv discovery; a specified empty, missing,
non-file or unreadable path fails startup. Explicit selection also fails when
PYTHON_DOTENV_DISABLED disables dotenv loading. Restart after changing the file.
This selects backend dotenv input only: shell launchers, Docker Compose and the
frontend retain their own environment loading. Values they already export win.
It does not select ENV profiles or isolate databases, storage or tenants.
See backend dotenv selection.
Custom blob storage backends accept blob_storage.backend: module.Class or
module:Class, including a single .py module importable by the Gateway.
The selected class must implement BlobStore; backend options go under
blob_storage.backend_config. See blob storage.
Viewed-image artifact URLs can use shared blobs when the serving Gateway lacks the local file. This fallback requires an existing thread record accessible to the caller; deleting that record revokes access even if checkpoint cleanup fails. When blob storage is disabled, a missing local file returns 404 without loading checkpoint state. Shared-store failures also return 404 and emit a warning with the exception type, without backend error details.
Running the Application
Deployment Sizing
Use the table below as a practical starting point when choosing how to run DeerFlow:
- These numbers cover DeerFlow itself. If you also host a local LLM, size that service separately.
- Linux plus Docker is the recommended deployment target for a persistent server. macOS and Windows are best treated as development or evaluation environments.
- If CPU or memory usage stays pinned, reduce concurrent runs first, then move to the next sizing tier.
Option 1: Docker (Recommended)
Requires Docker Desktop / Docker Engine and Docker Compose v2.24+
(docker compose version). Older Compose clients cannot parse the optional
env_file syntax in docker/docker-compose.yaml and docker/docker-compose-dev.yaml.
Development (hot-reload, source mounts):
make docker-init # Pull sandbox image (only once or when image updates)
make docker-start # Start services (auto-detects sandbox mode from config.yaml)
make docker-logs # View logs
make docker-start starts provisioner only when config.yaml uses provisioner mode (sandbox.use: deerflow.community.aio_sandbox:AioSandboxProvider with provisioner_url). Direct Compose users can add --profile provisioner to the dev command for the same mode; the default stack does not require Kubernetes.
With USERDATA_PVC_NAME, sandbox data, extra mounts and Lark broker credentials share one user-data volume with separate subPath mounts, avoiding duplicate references to the same claim on NFS CSI drivers.
Docker builds use the upstream uv registry by default. The gateway image installs with uv sync --locked, and uv.lock pins https://pypi.org/simple, so UV_INDEX_URL has to keep that value: exporting a mirror such as https://pypi.tuna.tsinghua.edu.cn/simple fails the build with The lockfile at `uv.lock` needs to be updated, but `--locked` was provided (reproduce in an already-synced environment with cd backend && UV_INDEX_URL=https://pypi.tuna.tsinghua.edu.cn/simple uv sync --locked --check; if the environment itself is stale, --check reports that instead for either index, so use uv lock --check to see it without syncing). Installing from a mirror requires regenerating the lock or dropping --locked, and both change what the image installs, so keep the default index for a checkout you intend to contribute back. NPM_REGISTRY=https://registry.npmmirror.com is independent of this: it applies to the frontend image's pnpm install and to the gateway image's Lark CLI install (@larksuite/cli, which backend/Dockerfile installs with npm).
Local AIO sandbox control traffic is always direct: loopback/private addresses,
single-label cluster hosts, and Docker/Podman internal hostnames do not inherit
HTTP_PROXY or HTTPS_PROXY. External sandbox FQDNs and public IPs still
honor environment proxy settings.
After native runtime shutdown and MCP teardown, Gateway closes an initialized sandbox provider, including AIO sandboxes first created by a worker-thread tool call. It does not start an unused sandbox provider solely for cleanup.
Backend processes automatically pick up config.yaml changes on the next config access, so model metadata updates do not require a manual restart during development.
Gateway runs use the top-level recursion_limit in config.yaml when an API
request does not provide one. The default is 100; valid per-request values
take precedence, and max_recursion_limit (default 1000) caps both. Changes
apply to the next run without restarting the Gateway. This top-level setting
applies to Gateway API and IM channel runs. A recursion_limit in an IM
channel's session.config takes precedence, and GitHub channel runs keep
their own floor. Embedded DeerFlowClient runs retain their own defaults and
per-call override paths. A run that reaches its limit stops with a plain message
that names up to five files it created in /mnt/user-data/outputs without
presenting them; send a message to continue. IM channels reply with the same
message.
A custom frontend can add an optional context.client to a Gateway run request:
a client name, capabilities such as artifacts or charts, and display
preferences. DeerFlow sanitizes it and passes the values to the agent as
untrusted, request-scoped hints, so skills can pick an output shape the client
can show. It is never an authorization or identity signal. See
Create Run.
Client hints are omitted from stored run configuration and must be sent again
for each run; later runs, including server-started tasks, do not inherit them.
The checkpoint storage settings database.checkpoint_channel_mode and
database.checkpoint_delta.snapshot_frequency (default 10) are exceptions:
both are frozen when the process first builds an agent (including through
DeerFlowClient) and require a process restart to change safely.
In delta mode, a run selecting an older checkpoint reports an error before rewriting
state if either the selected checkpoint or the current head has pending tool
execution. For Command(resume=...), other pending graph nodes also prevent the
rewrite, except for the synthetic input entry node. Resume pending work from the
latest checkpoint; to regenerate completed work, submit new input there. Full-mode
resumes and ordinary replay without pending execution keep their existing behavior.
The optional database.checkpoint_cache section (delta channel mode only)
caches materialized checkpoint histories: type is memory (default) or
redis, and max_entries: 0 disables the cache. The redis backend is
Gateway/async-only; the sync TUI/embedded path supports memory only. The
cache is performance-only — results are identical with it disabled — so it is
never frozen and workers sharing one checkpoint database may safely run
different cache settings. Returned state snapshots have independent nested values,
so editing one cannot change later checkpoint reads. Redis thread cleanup treats a configured key_prefix
as literal text, including glob metacharacters, so deletion stays within that
namespace and thread. Redis rejects malformed write records on ingestion; malformed
or wrong-channel cached histories are rebuilt in full from authoritative checkpoints.
Seeds and write values remain opaque to the cache: these checks do not validate
schema-specific state contents.
Checkpoint reads, valid neighboring writes and optional-cache cleanup continue
even if a warning handler fails.
Gateway checkpoint cleanup runs when the next run wins durable same-thread admission, before its first checkpoint read. It checks only the root head and its parent, removes at most one metadata-only duration leaf without pending writes, and preserves explicit checkpoint selections and delta replay state. See checkpoint operations for the retention constraints.
[!TIP] On Linux, if Docker-based commands fail with
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock, add your user to thedockergroup and re-login before retrying. See CONTRIBUTING.md for the full fix.
Production (builds images locally, mounts runtime config and data):
make up # Build images and start all production services
make down # Stop and remove containers
Access: http://localhost:2026
make up waits for the Gateway /health endpoint before reporting success.
If the Gateway does not become healthy within the startup window, deployment
exits non-zero and prints the container status plus recent Gateway logs. The
production image starts from its already-built environment and never resolves
or installs Python dependencies at container startup.
Images built from a checkout never contain its local configuration:
.dockerignore keeps config.yaml, config.yaml.bak, extensions_config.json
and mcp_config.json (at the root or in the legacy backend/ location), .env
and .env*.local files, local HTTPS keys (frontend/*.pem), and the credential
files the extension manager refuses inside backend/extensions/sources/ out of
the build context. Compose mounts the config and passes the environment at
runtime, so pushing or sharing an image does not publish those credentials.
A locally built frontend image therefore no longer picks up NEXT_PUBLIC_*
values from frontend/.env.local: next build inlines them into the client
bundle, Compose's environment reaches only next start, and frontend/Dockerfile
takes no build arguments for them. The Docker stack serves the frontend and
Gateway from one nginx origin and needs none of them.
If make up reports an unwritable runtime home or an unreadable persisted secret
after make docker-start, run the printed sudo chown -R <uid>:<gid> '<home>'
recovery command and retry. The check honors secret overrides from the shell or
.env, accepts readable read-only secret files, and leaves make down available
without reading or generating secrets.
Production Docker commands can select a root dotenv profile with
DEER_FLOW_COMPOSE_ENV_FILE=/srv/deer-flow/stage.env make up. The same file
feeds Compose interpolation, Gateway/provisioner variables, and deployment
secret checks. YAML and runtime-state locations remain separate selectors;
see production env files.
For persistent deployments, configure database.backend as sqlite or
postgres. The selected backend is shared by the LangGraph checkpointer,
LangGraph Store, and DeerFlow application data. The deprecated checkpointer
section, when present, overrides the first two for backward compatibility.
Gateway startup automatically repairs the missing run-change schema affecting some existing databases (#5516). The repair preserves run history and existing change positions; downgrading the repair to its predecessor also retains the schema and positions required by that version.
For lightweight single-process event persistence, run_events.backend: jsonl
keeps Unicode message content intact, including line and paragraph separators.
Existing valid JSONL records remain readable without rewriting the files.
For per-call usage audits, an immediate LLM response replay with populated usage updates both nested usage and top-level token counters, even if initial usage was absent or zero. The original request, model, and status metadata stay intact; flushed events are not rewritten. See the run event contract.
The unified nginx endpoint is same-origin by default and does not emit browser CORS headers. If you run a split-origin or port-forwarded browser client, set GATEWAY_CORS_ORIGINS to comma-separated exact origins such as http://localhost:3000; the Gateway then applies the CORS allowlist and matching CSRF origin checks.
When fine-grained authorization is enabled, Live Browser connections require threads:write as well as ownership of the thread, even when only viewing frames: the same connection can control the browser. Permission checks run when connecting. Restart Gateway after upgrading to disconnect sessions admitted by older code.
The bundled Docker, local and Helm nginx configs proxy Live Browser WebSocket upgrades with a 600-second read timeout for quiet streams. Helm uses nginx's default connection and send timeouts for the in-cluster Gateway. Any additional Ingress or reverse proxy must also allow WebSocket upgrades.
Browser login uses HttpOnly session cookies. The login page offers a "keep me signed in" option that extends the browser session when the request is HTTPS (including trusted X-Forwarded-Proto: https) or localhost HTTP. The localhost exception uses the direct request Host and ignores forwarded host headers. Public HTTP deployments, including many temporary sandbox URLs, fall back to session cookies by default. DeerFlow never stores the password in browser storage; the UI may remember only the email address.
If an authentication refresh fails during a temporary network outage, a visible tab rechecks the unknown session when connectivity returns or on a later tab switch. Reconnects received in a background tab are remembered until it becomes visible, without waiting for the tab-switch throttle. Identity is restored only after a successful auth response; a confirmed expired session or explicit logout still requires signing in again.
Password forms accept non-ASCII email addresses and trim surrounding email whitespace. Sign-in requires at least six password characters; creating an account or choosing a new password requires eight. These checks run before sending a request. Passwords, including surrounding spaces, are submitted unchanged.
OIDC/SSO authorization endpoints may include provider query parameters such as ?prompt=login. DeerFlow preserves those parameters when adding the login request, including for configured authorization_endpoint overrides.
After an SSO sign-in, DeerFlow returns you to the page that asked you to sign in instead of the default workspace. Opening the login page while already signed in does the same for its next target.
Administrators can suspend and restore accounts from Settings → Users (admin-only; the section is hidden for ordinary users). The page lists each account's email, role, and status and exposes the same enable/disable operation as PATCH /api/v1/admin/users/{id}; the last remaining active admin cannot be disabled, and the Gateway's rejection detail is shown verbatim. A disabled account is rejected by every authentication surface, and its live sessions end on the next request with a login page that states the account has been disabled. Password login for a disabled account deliberately still answers "incorrect email or password" so the state is not disclosed to whoever holds the credentials.
DeerFlow still uses Forwarded / X-Forwarded-* headers to recover the browser-facing scheme and origin behind a proxy. The bundled nginx sets X-Forwarded-Proto, but preserves an upstream HTTPS value and does not overwrite every forwarded header. Configure the outer trusted proxy to replace or strip client-supplied forwarding headers before traffic reaches DeerFlow.
[!IMPORTANT] The Gateway still owns active run tasks in process, so production defaults to a single Gateway worker (
GATEWAY_WORKERS=1). Multi-worker deployments require Postgres, the Redis stream bridge (stream_bridge.type: redis),run_ownership.heartbeat_enabled: true, andrun_events.backend: db; process-local memory/JSONL event stores cannot enforce singleton delivery receipts across workers. Kubernetes replicas run one worker per Pod, which the worker count cannot see: declare them withdeployment.multi_instance: true(orDEER_FLOW_MULTI_INSTANCE=1, which deploy tooling such as a Helm chart can set from its replica count) so the startup gate enforces the same prerequisites instead of staying inert. Declared instances that store credentials (channel_connections, or MCP servers using the OAuthrefresh_tokengrant, whose rotating tokens they share through the database) must also share one at-restDEER_FLOW_CREDENTIALS_KEY(the Helm chart andmake upgenerate it; back it up, see backend/docs/CONFIGURATION.md). The bridge shares SSE delivery and boundedLast-Event-IDreplay across workers. When a trimmed reconnect cursor cannot be verified for complete replay, or a subscriber that already established an empty-stream wait falls behind before its first delivery, Memory and Redis emit a machine-readable SSEgapevent instead of silently returning a partial replay; the Web UI reloads durable thread/event state and resumes from the retained tail. The Memory bridge can replay without recovery when the cursor is its exact last evicted event: every successor is still retained. Older or unverifiable evicted cursors still trigger recovery. An already-observed Redis stream removed during replay also signals a gap, so slow subscribers reload durable state instead of heartbeating forever after cleanup. Redis also tracks terminal stream producers with a short lease: if a worker dies after committing a terminal run status but before publishing END, SSE and/waitfinish after the lease expires instead of waiting for the retained stream's TTL. Lease reconciliation marks runs from dead workers as errors, persists their delivery receipts, publishes the terminal stream marker, schedules retained-stream cleanup, and updates the affected thread status. Redis END publication is atomic and idempotent: owner and recovery retries confirm an existing marker without appending another, events cannot be appended once END is the stream tail, and an unowned recovery retry after cleanup does not recreate the stream. Legacy streams with an earlier END followed by late data can still accept data. The Redis compatibility scan () also runs on first END publication; disable it only after old writers stop and all legacy replay buffers expire or are removed, then restart every Gateway to use tail-only END detection. SSE, , and internal stream consumers use (default ) for idle liveness checks; changing it requires a Gateway restart. Malformed Redis reconnect IDs live-tail new events instead of replaying the retained buffer, and the rolling retained-buffer TTL () remains a cleanup safety net rather than a run timeout. Failed-login counters and lockouts for ( / ) are kept in the shared table whenever the application database is SQLite or Postgres (, the default), so every replica enforces one lockout per client IP; keeps the historical per-process counter, which under N replicas hands an attacker N × guesses and logs a startup warning. IM chat-to-thread bindings live in the shared table whenever the application database is SQLite or Postgres (an existing is imported once at startup and renamed ); IM bot credentials entered in the web UI live in the shared table, encrypted with (an existing is imported once and then deleted), so a save on any instance reaches the provider's connection within one lease renewal; each provider's platform connection is held by one instance through the shared table — (default , on wherever the multi-process signal is) hands it to another instance when the holder dies, and the startup gate refuses while an IM channel other than GitHub is enabled (see ); follow-up buffers still need their own multi-worker coordination. To try this topology on one machine, starts two Gateways on shared Postgres, Redis and , and runs the cross-instance checks; see the local two-Gateway harness in .
The local two-Gateway harness disables IM providers under both
channelsandchannel_connectionsby default.DEERFLOW_MI_KEEP_CHANNELS=1preserves them;channels.leader_leasestays as configured.Cleaning up an in-memory run stream, or closing its bridge, releases retained payloads and signals a replay gap to subscribers with unfinished replay. They reload durable state instead of waiting on a detached buffer or consuming stale history. Established readers that fully consumed a terminal stream retain their normal
endsignal. A new reader whose initial attachment loses the race with cleanup receives a gap so it can recover the saved result.In-memory stream events retain the payload as it was published. Reusing a producer's data object or editing a received event does not change another subscriber's output or reconnect replay.
During SSE replay-gap recovery, the Web UI preserves a reconnect pointer claimed by another run while durable state is loading. An older recovery cannot overwrite that pointer when it rejoins its own stream. Even a stale foreign pointer is preserved, so the next reload may first check that run before active-run recovery rejoins, adding a reconnect cycle.
Store-only SSE and
/waitconsumers can finish after a heartbeat when run-manager or scheduled-task reconciliation recovers a dead worker's run, even if no END marker was published. Ordinary completions still wait for END to preserve tail events.In single-process JSONL deployments, cancelling an admitted event-store mutation waits for its background file I/O, rollback, and bookkeeping to settle before releasing the thread write lock. This prevents an older cancelled write from recreating deleted records or rolling back a later successful write. Cancellation can therefore wait on slow storage; it does not stop an in-flight filesystem operation. Callers still waiting to acquire the lock can cancel without starting a mutation. A batch spanning multiple threads drains its current thread group before propagating cancellation; subsequent thread groups do not start.
After a run publishes its terminal stream marker, its process-local
RunRecordremains available for the existing five-minute grace period before cleanup; durable run history remains available throughRunStore, while the stream bridge retains its delivery tail on its separate cleanup schedule.With in-memory run storage, replacing a run snapshot under another thread removes its previous thread membership. Old-thread history and usage queries exclude it, and deleting the old thread preserves the reassigned run.
Run cancellation may land on any Gateway worker. A non-owning worker now persists the interrupt or rollback request for the live owner, which observes it during lease renewal and performs the normal cancellation flow; load-balancer routing alone no longer produces a 409. The first accepted action wins even if a retry lands on the owner, and accepted cancellation competes atomically with owner completion. Dead owners still follow lease takeover and orphan recovery. Cancellation latency is therefore bounded by the lease heartbeat interval.
Cancelling a model recovery probe, including while it is queued or waiting to retry, lets the next call check whether the provider has recovered. Cancellation does not count as a provider failure or release another call's active recovery probe.
Model retries honor valid provider
Retry-Afterhints up to 24 hours, including waits longer than the local backoff cap. Malformed, non-finite, or over-limit hints use normal backoff instead. This applies to numeric and HTTP-date hints in LLM error handling and Codex's retry path, and integer-second hints in Claude's retry path. Codex prioritizes numericretry-after-mshints; past HTTP-date hints retry immediately.With lease heartbeat enabled, a transient RunStore renewal error is retried only until the last confirmed lease expires; the stale worker then cancels local execution and suppresses checkpoint, completion-hook, delivery-receipt, and thread-status finalization. A remote tool side effect already in flight may still be outside local cancellation.
Reconciliation uses an atomic takeover claim that re-checks the lease after candidate selection, so a successful owner renewal wins over orphan recovery and only one reconciler can report a run as recovered. When multiple Gateway workers share the Docker/AIO or E2B sandbox backend, also configure
sandbox.ownership.type: redis; E2B uses the leases during background startup and periodic reconciliation so duplicate/orphan cleanup cannot terminate a live peer's sandbox.
See CONTRIBUTING.md for detailed Docker development guide.
Upgrading an existing checkout
Keep config.yaml, .env, and extensions_config.json. Stop the services you
currently use, run git pull --ff-only, then start the same mode again. Do not run
make config or make docker-init again for a routine source upgrade. If the new
version requires configuration changes, run make config-upgrade before restarting.
See Operations and Troubleshooting
for the commands for each mode.
To continue a thread paused at a static interrupt_before or interrupt_after
breakpoint, submit input: null (or omit input) to its /runs, /runs/wait,
or /runs/stream endpoint. This continues pending graph work with the source
turn's knowledge scope from the selected checkpoint (the head by default).
An explicit empty object is new input, not a continuation; dynamic interrupts
still require command.resume. In delta mode, historical continuation rejects
rewrites that would discard pending execution; continue at the head instead.
Continuations retain the checkpoint's MCP background_tasks projection; only
dictionary input refreshes it from the task repository.
Old null-input runs stored as {} cannot safely match a keyed null-input retry
after upgrading and return 409. Check the original run before submitting a new key.
When rolling out the resume-command idempotency fix across multiple Gateway
workers or Pods, route all keyed resume submissions and retries (command.resume
with Idempotency-Key, on thread-scoped /runs, /runs/stream, or /runs/wait)
only to upgraded workers until every worker serving these endpoints is upgraded.
Older workers ignore the private resume identity and compare only input: with
input: null, retrying deny can incorrectly reuse an earlier approve run.
If the load balancer cannot isolate upgraded workers, pause keyed resume traffic
until the rollout finishes, or stop all old workers before restarting on the new
version. The additive database migration keeps old run-history readers compatible;
it does not make old workers safe for resume admission. An upgraded worker returns
409 when retrying an identity-less legacy resume run, even for the same decision;
inspect that run and the current thread state before deciding to submit a new
action. See the run API contract.
Custom run stores must explicitly support every supplied idempotency field.
Keyed input and resume requests return HTTP 503 before run admission when the
store cannot guarantee that support. The built-in Memory and SQL stores support
both fields; older custom stores with an explicit idempotency_key parameter
continue to accept keyed input requests.
Gateway runs also assign IDs to input messages that omit one before storing the run record and invoking the graph. This keeps the message visible as one turn when the Web UI reconnects to an active run started by an API client. During a rolling upgrade, route keyed runs with ID-less input and their retries to upgraded workers until all Gateway workers are upgraded; older workers may reject those retries with 409. See the run API contract.
Option 2: Local Development
If you prefer running services locally:
Prerequisite: complete the "Configuration" steps above first (make setup). make dev requires a valid config.yaml in the project root. Set DEER_FLOW_PROJECT_ROOT to define that root explicitly, or DEER_FLOW_CONFIG_PATH to point at a specific config file. Runtime state defaults to .deer-flow under the project root and can be moved with DEER_FLOW_HOME; skills default to skills/ under the project root and can be moved with DEER_FLOW_SKILLS_PATH. Run make doctor to verify your setup before starting.
Configuration checks and agent-storage migrations use the installed backend environment through uv run --no-sync --project backend from the repository root. This preserves relative runtime paths and installed optional dependencies; see the setup checks and agent-storage migration.
On Windows, run the local development flow from Git Bash. Native cmd.exe and PowerShell shells are not supported for the bash-based service scripts, and WSL is not guaranteed because some scripts rely on Git for Windows utilities such as cygpath.
The documented root make commands invoke repository .sh files through Bash
explicitly. They therefore continue to work from source archives or filesystems
that do not preserve POSIX executable bits. When calling a script directly from
such a checkout, use bash ./scripts/<name>.sh ....
-
Check prerequisites:
make check # Verifies Node.js 22+, pnpm, uv, nginxThe local
make check,make install,make dev, andmake startentry points use a directpnpmexecutable when available and otherwise fall back tocorepack pnpm. With native Windows Python, the shared runner checkspnpm.cmdbefore the genericpnpmlookup, which followsPATH/PATHEXTand may select an.exeor.batin the same or an earlier PATH directory. The Corepack fallback likewise checkscorepack.cmdbeforecorepack. POSIX Python keeps the generic names first, including when running under MSYS/Cygwin. The runner and diagnostics resolve repository paths absolutely, so these checks work regardless of the caller's current directory. Corepack runs fromfrontend/, so it honors thepackageManagerversion pinned infrontend/package.json; enabling a global pnpm shim is not required.make checkdecodes tool diagnostics as UTF-8 and preserves Unicode pnpm errors on non-UTF-8 hosts. Invalid output bytes are replaced so the checker can report dependency failures instead of aborting while decoding output. -
Install dependencies:
make install # Install backend + frontend dependencies + pre-commit hooksHook setup calls pre-commit through uv, so uv's tool directory need not be on
PATH. -
(Optional) Pre-pull sandbox image:
# Recommended if using Docker/Container-based sandbox make setup-sandboxReads the configured sandbox image from UTF-8
config.yaml, with or without a leading BOM, using LF or CRLF line endings. On macOS, a successful Apple Container pull completes this step even when Docker is not installed. If Docker is available, its image is also pulled. -
Start services:
make dev -
Access: http://localhost:2026
-
(Optional) Load sample memory data for local review: open
Settings > Memory, click Import memory, and select . The browser imports into the signed-in user's memory.