Problem or use case
Automations (scheduled tasks) are getting genuinely useful — recurring RRULE schedules, per-task
agent, model override, reasoning effort, cost cap, pinned host and workspace. But they live
completely outside the Projects organizing model, and that gap gets worse the more automations a
user accumulates.
Two concrete pain points:
1. An Automation cannot be filed into a Project. Today the only thing that can belong to a
Project is a session: membership is a project_id pointer on the conversation metadata row
(omnigent/db/db_models.py:648-651), set through PATCH /v1/sessions/{id}
(omnigent/server/routes/sessions/routes_core.py:1990-2025). The scheduled_tasks table
(omnigent/db/db_models.py:1430) has agent_id, model_override, reasoning_effort,
max_cost_usd, workspace, host_id, timezone, state — and no project column. The
Automations page (web/src/pages/TasksPage.tsx) is a single flat list filterable only by name and
active/paused state. A user with a dozen automations across three projects has no way to see
"the automations for this project", and the sidebar's project folders can't show them at all.
2. The sessions an Automation spawns land unfiled. When an automation fires, the conversation
it creates is given agent_id, title, resolved host_id and workspace, then model_override
and reasoning_effort (omnigent/server/scheduled/fire.py:589). project_id is never
passed, so it defaults to NULL
(omnigent/stores/conversation_store/sqlalchemy_store.py:860,983). Every scheduled run therefore
drops an orphan session into the unfiled bucket, and the user has to hand-file each one after the
fact. This is the sharper of the two problems: it generates recurring manual cleanup forever,
proportional to schedule frequency.
There is also a latent duplication angle: ProjectConfig already carries host_id, workspace,
agent_id, use_worktree and base_branch (web/src/lib/projectsApi.ts:21-44) — exactly the
fields a user re-enters by hand on every automation belonging to that project.
Proposed solution
Let an Automation be assigned to a Project, and have that assignment flow through to the sessions
it spawns.
Sketch (the specification should settle the details and the open questions below):
- Data model — add a nullable
project_id to scheduled_tasks, mirroring the conversation
metadata pointer: Uuid16(), nullable (NULL = unfiled), no DB foreign key per the schema's
no-FK rule, with an index supporting per-owner per-project listing.
- API — accept and return
project_id on POST /v1/scheduled-tasks and
PATCH /v1/scheduled-tasks/{id}; support GET /v1/scheduled-tasks?project=… filtering.
Ownership must be verified against the target project the same way session filing does
(a project owned by someone else must 404, not leak existence).
- Runtime — when an automation fires, the spawned conversation inherits the automation's
project_id so scheduled runs appear inside their project folder automatically instead of
accumulating unfiled.
- UI — a project picker on the Automation create/edit form, project grouping or a project
filter on the Automations page, and (optionally) automations surfaced within their project's
sidebar folder.
Alternatives considered
- Reuse the legacy
omni_project name-valued label instead of a first-class column. Rejected:
the label path is the older half of an existing dual-read that the codebase is already
migrating away from, it keys on mutable project name rather than id, and it would add a second
legacy consumer to something meant to shrink.
- Leave automations flat and only file the spawned sessions. This fixes the orphan-session
cleanup but not discoverability — the user still cannot answer "which automations belong to this
project", and the per-automation project would have to be inferred from run history.
- Derive the project from the automation's pinned
workspace. Rejected: workspace is a
filesystem path, projects are a user-facing organizing concept; several projects can share a
workspace and an automation may have no pinned workspace at all (it resolves at fire time).
- Do nothing / let users file each run by hand. This is the status quo, and its cost grows
linearly with schedule frequency.
Open questions for the specification
- Inheritance vs. explicit assignment at fire time — does the spawned session take the
automation's project unconditionally, and what happens if the project was deleted between
arming and firing? (Note DELETE /v1/projects/{id} deletes only the container row and leaves
member project_id values dangling — omnigent/server/routes/projects.py:145-147 — so a fired
automation can legitimately reference a project that no longer exists.)
- Project deletion semantics for automations — unfile them (null out), leave them dangling
like sessions do today, or block deletion while automations reference the project? The web UI
currently compensates for sessions by unfiling members before deleting
(web/src/hooks/useConversations.ts:1752-1794); automations need a deliberate answer rather
than an accidental one.
- Filter by id or by name? The Projects surfaces are inconsistent today: CRUD is by id while
GET /v1/sessions?project= filters by name with a dual-read union
(omnigent/stores/conversation_store/sqlalchemy_store.py:2494-2549). A new endpoint should
probably filter by id, but the spec must say so explicitly and justify diverging from the
existing session-side shape.
- Should
null be accepted to unfile? Session filing rejects explicit null with a 400 and
uses "" to unfile (routes_core.py:1995-2007). Consistency versus a cleaner API is a real
trade-off worth stating.
- Does assigning a project pre-fill the automation's agent/workspace/host from
ProjectConfig?
Tempting, but it risks silently changing where an existing automation runs. Probably
out of scope for a first cut — the spec should say yes or no rather than leave it ambiguous.
- Backfill — should existing automations, or the already-unfiled sessions their past runs
created, be retroactively filed? (Likely no, but state it.)
Type of change
Enhancement spanning DB migration, server API, scheduler runtime, and web UI.
Component
comp:server (API + scheduler runtime), comp:web-ui (Automations page + project picker)
Additional context
Grounding for the above (branch staging):
- Automations domain entity is deliberately named "scheduled task"; only UI copy says
"Automations" (omnigent/entities/scheduled_task.py:10).
- Fire path and the exact set of fields copied onto the spawned conversation:
omnigent/server/scheduled/fire.py:589.
- Automations API surface:
omnigent/server/routes/scheduled_tasks.py (create :289,
list :331, detail :365, runs :384, run-now :422, patch :465, delete :514).
Note responses are hand-built dicts rather than response models, so adding a field touches the
serializers directly (:113, :138).
- Project membership pointer and dual-read filter:
omnigent/db/db_models.py:648-651,
omnigent/stores/conversation_store/sqlalchemy_store.py:1362-1388,2494-2549.
- Alembic conventions: single lineage under
omnigent/db/migrations/versions/, additive,
no foreign keys, dialect-split for SQLite (recreate="always") vs PG/MySQL.
Problem or use case
Automations (scheduled tasks) are getting genuinely useful — recurring RRULE schedules, per-task
agent, model override, reasoning effort, cost cap, pinned host and workspace. But they live
completely outside the Projects organizing model, and that gap gets worse the more automations a
user accumulates.
Two concrete pain points:
1. An Automation cannot be filed into a Project. Today the only thing that can belong to a
Project is a session: membership is a
project_idpointer on the conversation metadata row(
omnigent/db/db_models.py:648-651), set throughPATCH /v1/sessions/{id}(
omnigent/server/routes/sessions/routes_core.py:1990-2025). Thescheduled_taskstable(
omnigent/db/db_models.py:1430) hasagent_id,model_override,reasoning_effort,max_cost_usd,workspace,host_id,timezone,state— and no project column. TheAutomations page (
web/src/pages/TasksPage.tsx) is a single flat list filterable only by name andactive/paused state. A user with a dozen automations across three projects has no way to see
"the automations for this project", and the sidebar's project folders can't show them at all.
2. The sessions an Automation spawns land unfiled. When an automation fires, the conversation
it creates is given
agent_id,title, resolvedhost_idandworkspace, thenmodel_overrideand
reasoning_effort(omnigent/server/scheduled/fire.py:589).project_idis neverpassed, so it defaults to
NULL(
omnigent/stores/conversation_store/sqlalchemy_store.py:860,983). Every scheduled run thereforedrops an orphan session into the unfiled bucket, and the user has to hand-file each one after the
fact. This is the sharper of the two problems: it generates recurring manual cleanup forever,
proportional to schedule frequency.
There is also a latent duplication angle:
ProjectConfigalready carrieshost_id,workspace,agent_id,use_worktreeandbase_branch(web/src/lib/projectsApi.ts:21-44) — exactly thefields a user re-enters by hand on every automation belonging to that project.
Proposed solution
Let an Automation be assigned to a Project, and have that assignment flow through to the sessions
it spawns.
Sketch (the specification should settle the details and the open questions below):
project_idtoscheduled_tasks, mirroring the conversationmetadata pointer:
Uuid16(), nullable (NULL= unfiled), no DB foreign key per the schema'sno-FK rule, with an index supporting per-owner per-project listing.
project_idonPOST /v1/scheduled-tasksandPATCH /v1/scheduled-tasks/{id}; supportGET /v1/scheduled-tasks?project=…filtering.Ownership must be verified against the target project the same way session filing does
(a project owned by someone else must 404, not leak existence).
project_idso scheduled runs appear inside their project folder automatically instead ofaccumulating unfiled.
filter on the Automations page, and (optionally) automations surfaced within their project's
sidebar folder.
Alternatives considered
omni_projectname-valued label instead of a first-class column. Rejected:the label path is the older half of an existing dual-read that the codebase is already
migrating away from, it keys on mutable project name rather than id, and it would add a second
legacy consumer to something meant to shrink.
cleanup but not discoverability — the user still cannot answer "which automations belong to this
project", and the per-automation project would have to be inferred from run history.
workspace. Rejected: workspace is afilesystem path, projects are a user-facing organizing concept; several projects can share a
workspace and an automation may have no pinned workspace at all (it resolves at fire time).
linearly with schedule frequency.
Open questions for the specification
automation's project unconditionally, and what happens if the project was deleted between
arming and firing? (Note
DELETE /v1/projects/{id}deletes only the container row and leavesmember
project_idvalues dangling —omnigent/server/routes/projects.py:145-147— so a firedautomation can legitimately reference a project that no longer exists.)
like sessions do today, or block deletion while automations reference the project? The web UI
currently compensates for sessions by unfiling members before deleting
(
web/src/hooks/useConversations.ts:1752-1794); automations need a deliberate answer ratherthan an accidental one.
GET /v1/sessions?project=filters by name with a dual-read union(
omnigent/stores/conversation_store/sqlalchemy_store.py:2494-2549). A new endpoint shouldprobably filter by id, but the spec must say so explicitly and justify diverging from the
existing session-side shape.
nullbe accepted to unfile? Session filing rejects explicitnullwith a 400 anduses
""to unfile (routes_core.py:1995-2007). Consistency versus a cleaner API is a realtrade-off worth stating.
ProjectConfig?Tempting, but it risks silently changing where an existing automation runs. Probably
out of scope for a first cut — the spec should say yes or no rather than leave it ambiguous.
created, be retroactively filed? (Likely no, but state it.)
Type of change
Enhancement spanning DB migration, server API, scheduler runtime, and web UI.
Component
comp:server(API + scheduler runtime),comp:web-ui(Automations page + project picker)Additional context
Grounding for the above (branch
staging):"Automations" (
omnigent/entities/scheduled_task.py:10).omnigent/server/scheduled/fire.py:589.omnigent/server/routes/scheduled_tasks.py(create:289,list
:331, detail:365, runs:384, run-now:422, patch:465, delete:514).Note responses are hand-built dicts rather than response models, so adding a field touches the
serializers directly (
:113,:138).omnigent/db/db_models.py:648-651,omnigent/stores/conversation_store/sqlalchemy_store.py:1362-1388,2494-2549.omnigent/db/migrations/versions/, additive,no foreign keys, dialect-split for SQLite (
recreate="always") vs PG/MySQL.