* fix(frontend): clarify reuse-thread scheduling behavior * fix(scheduler): enqueue overlapping scheduled runs * fix(scheduler): preserve queue lease fencing * fix(scheduler): close queue concurrency races * fix(scheduler): harden queue timeout bookkeeping * fix(scheduler): preserve manual failure schedule --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
18 KiB
DeerFlow Scheduled Tasks MVP Design
Date: 2026-07-01 Status: Approved for implementation Scope: First-class scheduled-task management for DeerFlow web workspace
Problem Statement
DeerFlow main does not ship a real scheduled-task product surface today. The repository already has internal timers, worker pools, and run persistence, but users cannot create, inspect, pause, resume, trigger, or delete durable background tasks from the product.
This creates three concrete problems:
- Users cannot automate recurring DeerFlow work such as daily summaries, periodic follow-ups, or recurring repo triage from the normal workspace.
- Existing cron-related PRs prove demand, but they either cut scope too broadly or start from the wrong interaction surface, which makes them hard to merge and harder to operate safely.
- Without a management surface, any future chat-created schedule would be operationally unsafe because users would have no first-class place to inspect or stop unattended jobs.
The first implementation must solve the operational and product-control problem before natural-language schedule creation.
Solution Summary
Build a scheduled-task MVP with these hard boundaries:
- Durable backend resource: add a
scheduled_taskresource with DB-backed persistence and DB-backed task-run history. - Shared execution path: scheduled executions must launch through the existing DeerFlow run lifecycle, not a parallel agent path.
- Workspace management page: add a first-class page at
/workspace/scheduled-tasksfor list/detail/create/edit/pause/resume/trigger/delete. - Execution context mode is explicit: every task chooses whether runs reuse an existing thread or create a fresh thread per occurrence.
- Minimal schedule surface: MVP supports
onceandcron, but notinterval. - Opt-in runtime gate: background scheduling remains disabled by default and requires explicit config enablement.
Explicit Non-Goals
The MVP intentionally does not include:
- Conversation-created schedules or a
schedule_tasktool. - Text-only notification jobs.
- Channel, IM, or GitHub dispatch targets.
- Goal-backed scheduled work.
- Retry/dead-letter orchestration.
- Distributed leader election beyond a single enabled scheduler instance with DB lease claims.
- Intervals shorter than 60 seconds for user-created tasks.
These exclusions are not optional polish cuts. They are what keeps the first PR reviewable.
Chosen Architecture
Why this shape
This MVP combines the right parts of prior DeerFlow cron attempts without inheriting their problems:
- Keep the execution discipline from the narrower backend MVPs: scheduler decides when to run, existing run services decide how to run.
- Keep the durable task identity + task history shape from broader implementations.
- Put management UI before chat-created scheduling, because users need a reliable control plane before background automation can be created from conversation.
Resource Model
The MVP introduces two durable entities:
scheduled_tasksscheduled_task_runs
scheduled_tasks is the durable trigger definition. scheduled_task_runs is the execution ledger per occurrence.
This keeps schedule identity separate from DeerFlow runs, which already model one concrete execution attempt.
User Stories
- As a DeerFlow user, I want to create a one-time task that can run in a fresh thread, so periodic automation does not silently accumulate old context.
- As a DeerFlow user, I want to create a recurring cron task that can either reuse a thread or create a fresh thread per run, so I can choose between continuity and isolation explicitly.
- As a DeerFlow user, I want to see next run time, last run result, and last error at a glance, so I know whether automation is healthy.
- As a DeerFlow user, I want to pause and resume a task, so I can stop automation without deleting configuration.
- As a DeerFlow user, I want to trigger a task manually, so I can test or re-run it on demand.
- As a DeerFlow user, I want to inspect task run history, so I can audit what happened.
- As a DeerFlow user, I want tasks to be owner-scoped, so no other user can list or mutate my automations.
- As a maintainer, I want scheduler execution to reuse existing run-launch code, so scheduled runs do not become a second runtime stack.
MVP Product Shape
Supported Task Kinds
Only one execution kind is supported in MVP:
task_type = "agent"dispatch_type = "thread"
That means every scheduled task is defined as:
- context mode
- optional target thread id
- title
- prompt override
- schedule definition
- runtime policy
When it fires, DeerFlow launches a normal run, but the execution thread is selected by context_mode.
These are fixed MVP semantics, not user-editable API fields and not persisted schema columns. The first PR must behave as if every task implicitly carries those values, without prematurely generalizing the contract.
Supported Schedule Kinds
The user-facing MVP supports:
oncecron
The MVP does not support interval because:
- it adds another schedule parser path,
- it enlarges frontend validation,
- it increases edge-case surface around cadence drift and minimum interval enforcement,
- it is not required to prove the scheduler architecture.
If later added, interval can be layered on the same resource model.
Execution Context Rule
The MVP supports two execution-context modes:
fresh_thread_per_run— default. Each scheduled occurrence creates a fresh DeerFlow thread.reuse_thread— optional. Each scheduled occurrence reuses an existing thread.
This is deliberate:
- recurring digests, summaries, and automation jobs should not silently accumulate context forever;
- follow-up and reminder use cases still need an explicit reuse mode;
- the scheduler definition stays separate from the execution thread used by each occurrence.
Backend Design
Persistence Layout
Add harness-owned persistence packages:
backend/packages/harness/deerflow/persistence/scheduled_tasks/backend/packages/harness/deerflow/persistence/scheduled_task_runs/
Add ORM registration in:
backend/packages/harness/deerflow/persistence/models/__init__.py
Add Alembic migration under:
backend/packages/harness/deerflow/persistence/migrations/versions/
scheduled_tasks schema
Fields:
id: string primary keyuser_id: owner user id, indexedthread_id: nullable target thread id, indexedcontext_mode:fresh_thread_per_run | reuse_threadassistant_id: nullable assistant id snapshottitle: user-visible task titleprompt: explicit prompt to send when the task runsschedule_type:once | cronschedule_spec: JSON payloadtimezone: IANA timezonestatus:enabled | paused | running | completed | failed | cancelledoverlap_policy: fixed toskipin MVP, still persisted explicitlymisfire_policy: fixed torun_oncein MVP, still persisted explicitlynext_run_at: UTC timestamp, indexedlast_run_at: nullable UTC timestamplast_run_id: nullable DeerFlow run idlast_thread_id: nullable DeerFlow thread id from the latest executionlast_error: nullable textlease_owner: nullable stringlease_expires_at: nullable UTC timestamprun_count: integermax_runs: nullable integercreated_atupdated_at
Not included in MVP schema:
dispatch_typedispatch_targettask_typesandbox_profiletrust_policycredential_scope
Reason: those are real future needs, but introducing dormant columns now weakens the first implementation and invites half-implemented policy behavior. The first PR should store only what it truly enforces.
scheduled_task_runs schema
Fields:
id: string primary keytask_id: foreign-key-like indexed link toscheduled_tasks.idthread_id: indexed for efficient thread-level lookuprun_id: nullable DeerFlow run idscheduled_for: UTC timestamptrigger:scheduled | manualstatus:queued | running | success | failed | skippederror: nullable textstarted_at: nullable UTC timestampfinished_at: nullable UTC timestampcreated_at
This run ledger is distinct from DeerFlow runs because:
- a scheduled occurrence may fail before a DeerFlow run is created,
- an overlap skip still deserves audit visibility,
- manual and scheduled triggers need explicit occurrence records.
Repository APIs
Create two repositories:
ScheduledTaskRepositoryScheduledTaskRunRepository
Required repository behavior:
- create/get/list/update/delete tasks
- owner-scoped search
- claim due tasks atomically
- update lease / clear lease
- record status transitions
- insert run history rows
- list task run history
Atomic due-claim API must operate in one DB transaction:
- find due enabled tasks
- skip tasks with live unexpired lease
- set
lease_owner,lease_expires_at, and temporarystatus="running" - return claimed rows
The scheduler service must not implement claim logic in Python-only in-memory filters.
Scheduler Runtime Design
Location
Runtime service lives under:
backend/app/scheduler/
Reason: it needs app-layer dependencies and shared run-launch services. Harness persistence remains app-agnostic.
Lifecycle
The scheduler starts during FastAPI lifespan only when config enables it.
Suggested config section:
scheduler:
enabled: false
poll_interval_seconds: 5
lease_seconds: 120
max_concurrent_runs: 3
min_interval_seconds: 60
There is no separate leader toggle in MVP. The DB lease is the operational guard. If deployments later require multi-instance topology, a leader dimension can be added in hardening.
Execution flow
For each poll cycle:
- fetch up to
max_concurrent_runsdue tasks via repository claim, - for each claimed task:
- create
scheduled_task_runsrow withstatus=queued, - compute and persist the next schedule before or immediately after launch,
- dispatch a normal DeerFlow run through shared run-launch helper,
- persist
last_run_id,last_run_at,run_count, task-run status, and error fields, - release lease.
- create
Shared run-launch helper
MVP must extract or reuse a non-router helper based on existing logic in:
Required property:
- manual API trigger and background scheduler trigger both call the same launch helper.
The helper takes:
- target thread id
- target assistant id
- prompt content
- authenticated owner context
- origin metadata indicating
scheduled_task_idandscheduled_trigger
Overlap semantics
The original MVP used one fixed overlap rule:
- if the target thread already has a pending/running run, record the occurrence as
skipped, updatenext_run_at, and do not launch another run.
This historical rule was superseded by the durable enqueue behavior documented in the current README: a busy occurrence waits in queued, survives Gateway restarts, and fails only after the configured queue timeout.
Misfire semantics
MVP uses one fixed misfire rule:
run_once
If the scheduler was down and multiple occurrences were missed, only the latest eligible missed occurrence runs when the scheduler comes back.
Reason:
- avoids backlog explosion,
- avoids unreviewed catch-up storms,
- keeps first implementation deterministic.
One-time task completion
For once tasks:
- successful dispatch marks task
completed - dispatch failure marks task
failed - task remains visible and queryable from UI/history after completion or failure
MVP uses soft retention, not destructive deletion.
Cron semantics
Cron rules:
- accept exactly 5 fields
- reject 6-field cron with seconds
- store explicit IANA timezone
- compute
next_run_atin UTC - normalize weekday semantics consistently and test them explicitly
The implementation must not silently depend on an ambiguous day-of-week interpretation.
API Design
Add REST routes under /api/scheduled-tasks.
Routes
GET /api/scheduled-tasksPOST /api/scheduled-tasksGET /api/scheduled-tasks/{task_id}PATCH /api/scheduled-tasks/{task_id}POST /api/scheduled-tasks/{task_id}/pausePOST /api/scheduled-tasks/{task_id}/resumePOST /api/scheduled-tasks/{task_id}/triggerDELETE /api/scheduled-tasks/{task_id}GET /api/scheduled-tasks/{task_id}/runsGET /api/threads/{thread_id}/scheduled-tasks
There is intentionally no dispatch-target discovery endpoint in MVP because the only target is an owned thread.
Request validation
Create:
- title required
- prompt required
- thread id required and must be owner-accessible
schedule_typerequiredoncerequires run timestampcronrequires valid 5-field cron- timezone required
Update:
- allow title/prompt/schedule/timezone changes
- disallow owner/thread reassignment across users
- disallow mutation while task is in temporary
runningstate if that would invalidate schedule semantics
Authorization
Owner checks are mandatory for:
- list
- detail
- run history
- patch
- pause
- resume
- trigger
- delete
- thread-scoped list
This should reuse existing auth patterns from thread/runs routers rather than inventing a new access scheme.
Frontend Design
Navigation
Add new workspace nav item:
/workspace/scheduled-tasks
This belongs beside existing high-level workspace surfaces in WorkspaceNavChatList, not hidden under settings.
Main page
Add page:
frontend/src/app/workspace/scheduled-tasks/page.tsx
The page includes:
- list table/cards
- filter bar
- create-task button
- detail drawer or side panel
List columns
- title
- thread title
- schedule summary
- status
- next run
- last run
- last result
- actions
Filters
MVP filters:
- status
- schedule type
- thread
No owner filter is needed in MVP because tasks are already owner-scoped.
Create/edit form
Fields:
- title
- thread selector
- prompt textarea
- schedule type:
once | cron - once datetime picker
- cron input
- timezone selector
Validation:
- prompt non-empty
- title non-empty
- once datetime must be in the future
- cron must be valid before submit
Detail view
Displays:
- full prompt
- thread link
- raw schedule config
- last error
- run history list
- actions: pause/resume/trigger/delete/edit
Thread-level entry point
Thread chat pages gain a visible entry point to view schedules for the current thread.
MVP behavior:
- small button/link in thread page header opens filtered scheduled-task page for current thread
It does not need a full embedded task manager in-thread. Reusing the main page keeps the first PR smaller.
State and Data Fetching
Frontend adds a small scheduled-tasks API layer under:
frontend/src/core/scheduled-tasks/
Recommended pieces:
- typed request/response models
- list/detail/run-history fetchers
- mutations for create/update/pause/resume/trigger/delete
- React Query hooks
This should follow the same shape the repo already uses for threads and feedback, not ad-hoc local fetch calls sprinkled through components.
Error Handling
Backend
Explicit failures that must surface cleanly:
- missing or deleted thread
- unauthorized owner access
- invalid cron
- invalid timezone
- task already paused/resumed
- trigger rejected due to active in-flight thread run
- scheduler launch failure before run creation
Failure must never cause infinite immediate retry loops.
Frontend
Users should see:
- inline form validation errors
- mutation toasts for pause/resume/trigger/delete
- visible failed state in task row
- visible
last_errorin details
The UI must not show a healthy-looking task row when the last scheduler attempt failed.
Testing Strategy
Backend unit tests
- valid and invalid cron expressions
- valid and invalid timezone handling
- weekday normalization semantics
- next-run computation across timezone boundaries and DST-sensitive cases
- one-time schedule status transitions
- due-task claim logic and lease expiry
- original overlap skip behavior (superseded by durable queue coverage)
- misfire
run_oncebehavior
Backend integration tests
- CRUD API with owner isolation
- thread-scoped task list route
- pause/resume/trigger/delete flows
- manual trigger creates a normal DeerFlow run through shared launch helper
- scheduler loop claims each due task once
- dispatch failure writes task and task-run errors correctly
- deleted thread does not hot-loop retries
Frontend unit tests
- scheduled-task nav item renders and routes
- list renders status/next run/last result
- create dialog validates form state
- action buttons settle correctly after API response
- detail drawer renders history and last error
Frontend E2E
Playwright with mocked APIs:
- list page loads
- create task from UI
- pause/resume/trigger/delete flows
- thread header link navigates to filtered scheduled-task view
Real-path validation
Required before claiming feature complete:
- start backend and frontend
- create a one-time task due soon from the real UI
- observe row move through live status updates
- confirm linked DeerFlow run exists
- confirm completed/failure state is visible in the management page
Documentation Updates Required
If code lands, update:
README.mdwith feature overview and enablement noteAGENTS.mdandbackend/AGENTS.mdwith scheduler/runtime ownership and commands if architecture changes- config docs for new
schedulersection
Code Review Checklist
- Scheduled runs reuse the existing run lifecycle.
- Harness persistence does not import
app.*. - Due-task claim logic is atomic.
- No hot loop after dispatch failure.
- Day-of-week semantics are explicit and tested.
- Owner checks cover list/detail/history/mutate/trigger/delete.
- UI shows failing state honestly.
- Background scheduler remains opt-in.
- Thread-level entry point does not introduce duplicate management UI logic.
- Chat-created scheduling remains absent from MVP.
Implementation Order
- Backend persistence and repository layer
- Schedule parser / next-run computation
- Shared run-launch helper
- Scheduler service and API
- Frontend API layer and page
- Thread header entry point
- E2E and real-path validation
This order is mandatory because the frontend cannot be implemented against an unstable backend contract.