Skip to content

feat(server): add Routines/Cron primitive and scheduler (#2284)#2315

Open
ashishSoni1234 wants to merge 5 commits into
different-ai:devfrom
ashishSoni1234:feat/cron-routines-2284
Open

feat(server): add Routines/Cron primitive and scheduler (#2284)#2315
ashishSoni1234 wants to merge 5 commits into
different-ai:devfrom
ashishSoni1234:feat/cron-routines-2284

Conversation

@ashishSoni1234

@ashishSoni1234 ashishSoni1234 commented Jun 18, 2026

Copy link
Copy Markdown

Summary

  • Added support for backend "Routines" (cron jobs). This introduces a new primitive and a background CronScheduler service to automatically trigger open-ended tasks (like Claude co-work) on a recurring schedule.

Why

  • To allow power users and non-technical users alike to define recurring agents/tasks (e.g., "fetch this info and send that in slack every hour") without needing an external job orchestrator.

Issue

Scope

  • Added RoutineItem primitive in types.ts.
  • Built robust file-based CRUD in routines.ts and server.ts API endpoints (storing in .opencode/routines/<name>.md).
  • Built CronScheduler (scheduler.ts) using cron-parser to evaluate schedules every minute.
  • Automated creation of background agent sessions (opencode.session.create()) upon cron trigger.

Out of scope

  • Full UI components/settings page for visual routine management (laying the backend foundation first).

Testing

Ran

  • pnpm run typecheck across all server packages
  • Automated linting pipelines

Result

  • pass/fail: pass
  • if fail, exact files/errors: N/A

CI status

  • pass: Not run yet (waiting on CI hook)
  • code-related failures: N/A
  • external/env/auth blockers: N/A

Manual verification

  1. Verified API accepts cron parameters and persists them correctly with .md frontmatter.
  2. Verified cron-parser evaluates valid schedules and handles malformed strings without crashing the background thread.
  3. Verified the scheduler isolates execution strictly to the matched minute boundary avoiding duplicated triggers.

Evidence

  • N/A (Backend logic/primitive addition)

Risk

  • Low. The CronScheduler isolates exceptions per routine. If a schedule fails to parse, it skips it and logs an error, preventing the server from crashing. Time validation acts upon isolated boundaries.

Rollback

  • Revert this PR. There are no breaking database migrations; the primitive relies on standard .md file deletion if needed.

Review in cubic

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
openwork-landing Ready Ready Preview, Comment, Open in v0 Jun 19, 2026 5:43pm

@vercel

vercel Bot commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

@ashishSoni1234 is attempting to deploy a commit to the Different AI Team on Vercel.

A member of the Team first needs to authorize it.

@ashishSoni1234

Copy link
Copy Markdown
Author

Hi @dhruv-db, I have built the core backend primitive for Crons/Routines to solve your request! The CronScheduler is now actively parsing and dispatching tasks on schedule.

@obchain @Abhijeet1005 - The backend foundation and API surface for the Routines feature is ready for review. I've mirrored the skills/commands file primitive pattern, handled robust cron-parser evaluations, and ensured exactly-once triggers per interval.

Please take a look when you get a chance! Also, let me know if we need vercel deployment authorization to run the CI checks.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

4 issues found across 9 files

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread apps/server/src/scheduler.ts
Comment thread apps/server/src/scheduler.ts
Comment thread apps/server/src/routines.ts
Comment thread apps/server/src/server.ts Outdated

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 3 files (changes from recent commits).

Reply with feedback, questions, or to request a fix.

Re-trigger cubic

Comment thread apps/server/src/scheduler.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread apps/server/src/scheduler.ts Outdated
@devcool20

Copy link
Copy Markdown
Contributor

Hey @ashishSoni1234, thanks for putting this together! The routines scheduler is a great addition. I did a quick local review and run-through on Windows, and found a few critical runtime bugs and quality fixes that we need to address before this can be merged:

  1. Critical Runtime / Logic Bugs:
  • Invalid SDK Payload: In apps/server/src/scheduler.ts (around line 103), the parameters for opencode.session.prompt are passed at the root of the options object (e.g. sessionID, text, mode). The SDK client actually expects path.id for the session ID and body.parts for the text payload. This will crash at runtime.
  • Blocking Call: The scheduler currently calls the blocking session.prompt method which blocks the loop thread until the agent completes. We should switch this to opencode.session.promptAsync so it returns a 204 immediately and runs in the background.
  • Empty Schedule parsing: If a routine is created with an empty schedule "", it currently bypasses parsing validation in routines.ts but is still loaded. The scheduler then attempts to parse the empty string on every tick, logging parsing errors. We should skip routines with missing/empty schedules in listRoutinesInDir.
  1. Project Rules & Typing:
  • Avoid 'any' types: There are a few instances of 'any' types used in scheduler.ts (for client creation callbacks and triggerRoutine parameters). To align with our project guidelines, we should import the types (e.g., RoutineItem) and type these explicitly.
  1. Missing Features / Integration:
  • Workspace Import/Export: The new routines primitive is missing from the exportWorkspace and importWorkspace pipelines in server.ts and workspace-import-preview.ts. Currently, if a user exports and re-imports a workspace, all of their routines will be lost.
  1. Performance & Robustness:
  • Redundant Disk I/O: In the catch-up scheduler loop, listRoutines is called on disk inside every tick iteration. We should read the routines once per tick and reuse the list in memory.
  • Overlapping executions: Consider adding a set to track running routine IDs/names to prevent the same routine from triggering multiple concurrent processes if one runs longer than 1 minute.

@ashishSoni1234

Copy link
Copy Markdown
Author

Hi @devcool20, thanks for the thorough review and great catches!

I've pushed a new commit that addresses all your points and the AI reviewer's feedback:

  1. SDK Payload & Blocking: Fixed the .prompt parameter signature to expect path.id and body.parts. I've also wrapped the session dispatch into an async non-blocking execution block (executeRoutineInBackground). This prevents blocking the tick() thread while correctly maintaining a runningRoutines Set to avoid overlapping duplicate dispatches of the same routine.
  2. Empty Schedule: Added strict validation inside listRoutinesInDir to silently ignore invalid/empty cron strings instead of spamming parse errors every minute.
  3. Workspace Pipeline: fully mapped the routines primitive into the exportWorkspace and importWorkspace preview pipelines alongside skills and commands. Exported workspaces will now retain their routines correctly!
  4. Performance & P1: Hoisted the disk reading listRoutines outside the catchup loop so I/O is reduced per minute tick. Also fixed the P1 clock-drift issue identified by cubic, strictly assigning lastTickMinute = targetMinute on early breaks.
  5. Types: Purged any instances using WorkspaceInfo and RoutineItem.

Let me know if this looks good to go!

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 4 files (changes from recent commits).

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

Comment thread apps/server/src/scheduler.ts

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1 issue found across 1 file (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="apps/server/src/scheduler.ts">

<violation number="1" location="apps/server/src/scheduler.ts:93">
P2: Sequential promise queue for routine execution can grow unboundedly if triggerRoutine hangs or is very slow, causing memory leaks and stale accumulated executions. The previous `runningRoutines` Set skipped overlapping triggers to bound concurrency; the new `routineQueues` Map chains every trigger indefinitely without a queue depth limit or execution timeout.</violation>
</file>

Tip: Review your code locally with the cubic CLI to iterate faster.

Re-trigger cubic

}
}

private executeRoutineInBackground(workspace: WorkspaceInfo, routine: RoutineItem) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Sequential promise queue for routine execution can grow unboundedly if triggerRoutine hangs or is very slow, causing memory leaks and stale accumulated executions. The previous runningRoutines Set skipped overlapping triggers to bound concurrency; the new routineQueues Map chains every trigger indefinitely without a queue depth limit or execution timeout.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At apps/server/src/scheduler.ts, line 93:

<comment>Sequential promise queue for routine execution can grow unboundedly if triggerRoutine hangs or is very slow, causing memory leaks and stale accumulated executions. The previous `runningRoutines` Set skipped overlapping triggers to bound concurrency; the new `routineQueues` Map chains every trigger indefinitely without a queue depth limit or execution timeout.</comment>

<file context>
@@ -90,18 +90,29 @@ export class CronScheduler {
   }
 
-  private async executeRoutineInBackground(workspace: WorkspaceInfo, routine: RoutineItem) {
+  private executeRoutineInBackground(workspace: WorkspaceInfo, routine: RoutineItem) {
     const key = `${workspace.id}:${routine.name}`;
-    if (this.runningRoutines.has(key)) {
</file context>
Suggested change
private executeRoutineInBackground(workspace: WorkspaceInfo, routine: RoutineItem) {
private executeRoutineInBackground(workspace: WorkspaceInfo, routine: RoutineItem) {
const key = `${workspace.id}:${routine.name}`;
if (this.routineQueues.has(key)) {
console.warn(`Skipping overlapping routine trigger for ${routine.name}`);
return Promise.resolve();
}
const promise = this.triggerRoutine(workspace, routine)
.catch((err) => {
console.error(`Error in background routine ${routine.name}:`, err);
})
.finally(() => {
this.routineQueues.delete(key);
});
this.routineQueues.set(key, promise);
return promise;
}

@Pablosinyores Pablosinyores left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

scheduler.ts imports createWorkspaceOpencodeClient from ./server.js, but the class only ever uses the injected this.createClient callback — the direct import is unused. Since server.ts imports scheduler.ts, that unused import sets up a server <-> scheduler circular dependency for no reason (the inline comment // We need to export this or pass a callback looks like a leftover from before the switch to callback injection). Dropping the import removes the cycle, and the callback pattern is the right call.

A few smaller things:

  • CronExpressionParser.parse(routine.schedule, { currentDate: ... }) runs in the servers local timezone with no per-routine tz. For a user-facing "every hour" / "every day at 9" feature this fires in server time, which can surprise users in other zones — worth either documenting it or threading a timezone through RoutineItem.
  • triggerRoutine already wraps its body in try/catch and logs, and executeRoutineInBackground wraps the same call again with another .catch. The inner handling makes the outer one redundant.
  • The SDK return-shape sniffing ("data" in result) suggests the client is typed any. If createWorkspaceOpencodeClient exported its return type, session.creates result could be typed and the runtime shape check dropped.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Please allow to set Routines or Crone jobs like Claude cowork

3 participants