* feat: add scheduled tasks MVP
* fix: harden scheduled task execution semantics
* feat(scheduled-tasks): preset-driven schedule form with timezone and live preview
Replace the raw cron input with a preset Select (hourly/daily/weekly/monthly/custom)
plus structured inputs (time picker, weekday toggles, day-of-month), datetime-local
for one-time tasks, a timezone selector defaulting to the browser timezone, and a
live human-readable preview. Reuses one ScheduledTaskScheduleInput for create and
edit; backend contract unchanged; zero new deps (pure Intl + DST-safe offset helpers).
* feat(scheduled-tasks): full-page i18n + recipe templates + E2E locale pin
Localize the rest of the scheduled-tasks page (filters, detail pane, actions,
edit form, run list, enum values) via t.scheduledTasks.* in en/zh. Add four
built-in recipe templates (GitHub Trending, news digest, issue triage, weekly
report) exposed as a chip row that pre-fills title + prompt + schedule. Pin
Playwright locale to en-US so E2E selectors stay stable against i18n. No backend
change, no new deps.
* fix(scheduled-tasks): idempotent 0003 migration, update head constants, future-date once test
Merge with main surfaced three CI failures:
- 0003_scheduled_tasks create_table collided with legacy test seeds that
build from full metadata; guard with inspector.has_table so the revision
no-ops when the table already exists (0004/0005 are already idempotent via
_helpers.py).
- persistence bootstrap concurrency/regression tests pinned HEAD to main's
0002_runs_token_usage; bump to the new head 0005_scheduled_task_thread_nullable.
- once-task router test used a fixed past run_at and tripped the
must-be-in-the-future validation; use a future date.
* address review: ok-check, 502 for trigger failure, mock fields, migration filename, doc fences
- fetchThreadScheduledTasks now checks response.ok like the other fetchers.
- trigger endpoint returns 502 (not 409) when dispatch fails outright, so
clients can distinguish a real conflict from a server-side failure.
- E2E mock normalizes scheduled-task objects with context_mode/last_thread_id
and nullable thread_id, matching the backend contract the UI renders against.
- Rename 0002_scheduled_tasks.py -> 0003_scheduled_tasks.py to match its
revision id (file was renamed in spirit already; filename now follows).
- CONFIGURATION.md: close the Tool Groups yaml fence and drop the stray fence
after the Scheduler notes so the sections render correctly.
* fix(scheduled-tasks): harden lease, poller, config, and frontend UX after review
* fix(scheduled-tasks): harden run lifecycle, overlap skip, non_interactive gating, and DST conversion after review
- defer a once task's terminal status to the run-completion hook; the task
stays running until the real outcome, and a startup sweep cancels once
tasks orphaned by a crash (launch-time 'completed' could stick forever)
- record interrupted runs as a distinct 'interrupted' run status with a
readable message; an interrupted once task ends 'cancelled', not 'failed'
- enforce overlap_policy=skip for fresh_thread_per_run via an active-run
pre-check (same-thread ConflictError can never fire across fresh threads)
- protect terminal run statuses from the late launch-path 'running' write
- honor context.non_interactive only for internally-authenticated callers;
arbitrary clients can no longer strip ask_clarification
- fix DST-stale timezone offset in zonedLocalToUtcIso by re-deriving the
offset at the resolved instant (once tasks fired an hour late around
spring-forward and the create->edit round-trip diverged)
- drop dead ScheduledTaskRunRepository.update_by_run_id; share one Gateway
API error helper between channels and scheduled-tasks frontends
* fix(scheduled-tasks): close review round-3 gaps in guards, concurrency, and API ergonomics
- scrub internal-only context keys (non_interactive) from the assembled run
config for non-internal callers: gating body.context alone left the same
key smuggle-able through the free-form body.config copied verbatim by
build_run_config
- guard update_after_launch with protect_terminal so the launch bookkeeping
write cannot clobber a once task already finalized by a fast-failing run's
completion hook (parent-row sibling of the run-row guard)
- reject a manual trigger while the task has an active run (409) instead of
launching a duplicate concurrent run on fresh_thread_per_run
- re-arm a terminal once task to enabled when PATCH pushes run_at into the
future; previously the endpoint returned 200 with a next_run_at that could
never be claimed
- make max_concurrent_runs a real global cap: each poll claims only into the
remaining budget of active (queued/running) scheduled runs
- paginate GET /scheduled-tasks/{id}/runs (limit<=200, offset) and push the
thread filter of /threads/{id}/scheduled-tasks into SQL
- stamp context.user_id on scheduler-launched runs, matching IM channels, so
user-scoped guardrail providers see the owning user
---------
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
MVP uses 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 is intentionally narrower than exposing multiple overlap policies in the first PR.
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
- overlap skip behavior
- 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.