Skip to content

feat(cli): schedule notebooks in Deepnote Cloud - #437

Merged
jamesbhobbs merged 16 commits into
mainfrom
feat/cloud-scheduling
Aug 5, 2026
Merged

jamesbhobbs merged 16 commits into
mainfrom
feat/cloud-scheduling

Conversation

@jamesbhobbs

@jamesbhobbs jamesbhobbs commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a typed Deepnote Cloud schedule API client
  • add reusable scheduleInCloud support to the local runner, including create-if-missing behavior
  • add deepnote schedule with hourly, daily, weekly, monthly, and custom cron options
  • add timezone, multi-notebook selection, JSON output, browser opening, shell completions, tests, and documentation

Behavior

Scheduling configures the project recurring Cloud schedule without immediately running the notebook. Deepnote supports one scheduled notebook per project, so invoking the command again updates or re-points that schedule.

This work is separate from the pipeline/orchestration changes.

Validation

  • pnpm test: 2,684 passed, 1 opt-in integration test skipped
  • pnpm typecheck
  • pnpm biome:check
  • pnpm prettier:check
  • pnpm spell-check
  • pnpm build

Conflict note

Most implementation is in new files. The small CLI registration, completion, documentation, and Cloud export overlaps with #433 may require a minor conflict resolution depending on merge order.

Summary by CodeRabbit

  • New Features
    • Added deepnote schedule <path> for recurring hourly, daily, weekly, monthly, or cron-based Cloud runs.
    • Added scheduling to local-runner, including a Schedule control in the run-app example.
    • Supports notebook selection, timezones, JSON output, browser opening, and creating missing Cloud projects.
  • Bug Fixes
    • Improved Cloud notebook creation and coordination for run and schedule operations.
    • Added protection against cross-origin Cloud mutations.
  • Documentation
    • Added CLI, local-runner, and scheduling examples and reference guidance.
  • Chores
    • Updated shell completions with scheduling commands and options.

@coderabbitai

coderabbitai Bot commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 37572fae-f0f8-48b5-81b8-ab4c01a7dc36

📥 Commits

Reviewing files that changed from the base of the PR and between 7177a2e and 34b4c0d.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (1)
  • package.json

📝 Walkthrough

Walkthrough

Adds recurring Deepnote Cloud scheduling across Cloud APIs, local-runner orchestration, the CLI, static-server endpoints, and example UI flows. The implementation validates schedule expressions, resolves or creates notebooks, upserts schedules, supports text and JSON output, and coordinates concurrent cloud operations. Tests, exports, completions, examples, dependencies, and documentation are updated.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant scheduleInCloud
  participant DeepnoteCloud
  CLI->>scheduleInCloud: local file, cron, timezone
  scheduleInCloud->>DeepnoteCloud: resolve or create notebook
  scheduleInCloud->>DeepnoteCloud: upsert recurring schedule
  DeepnoteCloud-->>scheduleInCloud: schedule result
  scheduleInCloud-->>CLI: formatted result
Loading

Possibly related issues

Possibly related PRs

  • deepnote/deepnote#364: Provides notebook and init-notebook resolution used by the scheduling flow.
  • deepnote/deepnote#419: Provides local-runner, cloud project, and static-serving infrastructure extended by this PR.

Suggested reviewers: dinohamzic

🚥 Pre-merge checks | ✅ 4 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 63.64% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Updates Docs ⚠️ Warning Documentation is complete in the OSS repo. However, the custom check requires roadmap updates in deepnote-internal private repo, which is not accessible in this environment. Verify that the roadmap in deepnote-internal has been updated to reflect the new schedule feature before merging.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main change: adding CLI support to schedule notebooks in Deepnote Cloud.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 97.21627% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.78%. Comparing base (0e58ac8) to head (34b4c0d).

Files with missing lines Patch % Lines
packages/cli/src/commands/schedule.ts 94.36% 4 Missing ⚠️
packages/cli/src/cli.ts 25.00% 3 Missing ⚠️
packages/local-runner/src/schedule-in-cloud.ts 94.33% 3 Missing ⚠️
packages/local-runner/src/serve-static.ts 96.29% 2 Missing ⚠️
packages/cloud/src/schedules.ts 96.77% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #437      +/-   ##
==========================================
+ Coverage   87.36%   87.78%   +0.42%     
==========================================
  Files         181      187       +6     
  Lines        9494     9927     +433     
  Branches     2624     2768     +144     
==========================================
+ Hits         8294     8714     +420     
- Misses       1199     1212      +13     
  Partials        1        1              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (2)
packages/cli/src/commands/schedule.ts (1)

70-77: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

No progress feedback wired for project creation during scheduling.

onWarning is forwarded to scheduleInCloud, but onCreateProgress isn't, so creating a brand-new cloud project as part of schedule gives no feedback while it runs (per the local-runner contract, createFromFile accepts onCreateProgress).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/schedule.ts` around lines 70 - 77, Update the
schedule command’s scheduleInCloud call to pass an onCreateProgress callback
alongside onWarning, reusing the local-runner progress behavior so project
creation reports feedback during scheduling. Preserve the existing scheduling
options and warning handling.
packages/cli/src/commands/schedule.test.ts (1)

123-127: 📐 Maintainability & Code Quality | 🔵 Trivial

Consider covering the browser-open failure and negative-open paths.

No test exercises openInBrowser rejecting (see the propagation issue flagged in schedule.ts lines 79-81) or confirms openInBrowser is skipped when --open isn't passed. Worth adding once that fix lands.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/schedule.test.ts` around lines 123 - 127, Extend
the schedule command tests around createScheduleAction to cover both open paths:
verify openInBrowser is not called when options({ open: false }) is used, and
mock it to reject when options({ open: true }) is used, asserting the command
propagates the failure. Keep the existing successful browser-open assertion
unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/schedule.ts`:
- Around line 79-81: Guard the best-effort openInBrowser call in
createScheduleAction so failures are swallowed after scheduleInCloud succeeds.
Apply the same non-failing promise handling pattern used by the local-runner
side effect, while preserving the existing options.open and result.viewUrl
conditions and successful command/JSON output.

In `@packages/local-runner/src/schedule-in-cloud.ts`:
- Around line 74-98: Update the lookup and creation flow around findNotebook and
createFromFile so a missing notebook in an existing same-named project does not
create a duplicate project. Resolve the project independently using the exact
project name, then add the notebook to that project; if the project cannot be
resolved, fail with a clear error. Preserve the existing found-notebook and
createIfMissing behavior.
- Line 58: Update the request body construction in the schedule-in-cloud flow so
an explicitly supplied timezone, including an empty string, is preserved instead
of omitted; distinguish absence from falsy values when checking
options.timezone, allowing upsertNotebookSchedule to perform its existing
validation.

---

Nitpick comments:
In `@packages/cli/src/commands/schedule.test.ts`:
- Around line 123-127: Extend the schedule command tests around
createScheduleAction to cover both open paths: verify openInBrowser is not
called when options({ open: false }) is used, and mock it to reject when
options({ open: true }) is used, asserting the command propagates the failure.
Keep the existing successful browser-open assertion unchanged.

In `@packages/cli/src/commands/schedule.ts`:
- Around line 70-77: Update the schedule command’s scheduleInCloud call to pass
an onCreateProgress callback alongside onWarning, reusing the local-runner
progress behavior so project creation reports feedback during scheduling.
Preserve the existing scheduling options and warning handling.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2eb48917-4ad0-4e24-86b3-08c98f91e719

📥 Commits

Reviewing files that changed from the base of the PR and between 126ae4e and d75be31.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (19)
  • packages/cli/README.md
  • packages/cli/package.json
  • packages/cli/src/cli.test.ts
  • packages/cli/src/cli.ts
  • packages/cli/src/commands/schedule.test.ts
  • packages/cli/src/commands/schedule.ts
  • packages/cli/src/completions.ts
  • packages/cli/src/utils/schedule-expression.test.ts
  • packages/cli/src/utils/schedule-expression.ts
  • packages/cloud/src/index.ts
  • packages/cloud/src/schedules.test.ts
  • packages/cloud/src/schedules.ts
  • packages/local-runner/README.md
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/schedule-in-cloud.test.ts
  • packages/local-runner/src/schedule-in-cloud.ts
  • skills/deepnote/SKILL.md
  • skills/deepnote/references/cli-schedule.md

Comment thread packages/cli/src/commands/schedule.ts
Comment thread packages/local-runner/src/schedule-in-cloud.ts Outdated
Comment thread packages/local-runner/src/schedule-in-cloud.ts Outdated
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 29, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/local-runner/src/serve-static.ts`:
- Around line 172-193: Protect the POST /api/schedule-cloud handler by
validating any supplied Origin against this server’s own origin before reading
or processing the scheduling request; reject mismatches with HTTP 403 while
allowing same-origin and absent-Origin requests. Add a test covering a foreign
Origin and asserting the 403 response, reusing the server’s existing
origin-validation utilities or request-handling symbols.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: cac69ecc-d68d-40e7-b7bb-3f120cb4b7c6

📥 Commits

Reviewing files that changed from the base of the PR and between 47211fb and 0e78fe7.

📒 Files selected for processing (14)
  • examples/local-runner-showcase.deepnote
  • examples/local-runner/README.md
  • examples/local-runner/run-app/README.md
  • examples/local-runner/run-app/index.html
  • examples/local-runner/run-app/serve.mjs
  • examples/local-runner/schedule-cloud.mjs
  • examples/scheduled-cloud-run.deepnote
  • package.json
  • packages/local-runner/README.md
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/schedule-in-cloud.test.ts
  • packages/local-runner/src/schedule-in-cloud.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/serve-static.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/local-runner/README.md

Comment thread packages/local-runner/src/serve-static.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/local-runner/src/schedule-in-cloud.ts (1)

77-115: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the shared "find-or-create cloud notebook" logic into one helper.

Both files implement the same sequence — coordinate via coordinateCloudNotebook, findNotebook by name, then findProject + createFromFile(destination) if missing — differing only in the inputs payload and options passed through. Based on learnings, this exact logic already needed the same duplicate-project fix applied twice (once per file); centralizing it avoids that recurrence.

  • packages/local-runner/src/schedule-in-cloud.ts#L77-L115: replace this block with a call to a shared resolveOrCreateCloudNotebook(...) helper, passing inputs: {} and the schedule's progress/warning callbacks.
  • packages/local-runner/src/run-in-cloud.ts#L142-L180: replace this block with the same shared helper, passing the run's inputs and full options.
♻️ Suggested shared helper sketch
export async function resolveOrCreateCloudNotebook(
  baseUrl: string,
  token: string,
  file: DeepnoteFile,
  localId: string,
  notebookName: string | undefined,
  allowCreate: boolean,
  notFoundError: unknown,
  createOptions: { inputs: Record<string, unknown> } & Pick<RunInCloudOptions, 'onCreateProgress' | 'onWarning'>
): Promise<CloudNotebookResolution> {
  return coordinateCloudNotebook(
    { baseUrl, token, projectName: file.project.name, notebookId: localId, notebookName, allowCreate },
    async () => {
      const found = await findNotebook(baseUrl, token, { projectName: file.project.name, notebookName })
      if (found) return { notebookId: found.notebookId, projectId: found.projectId, created: false }
      if (!allowCreate) throw notFoundError
      const project = await findProject(baseUrl, token, file.project.name)
      const createdTarget = await createFromFile(
        baseUrl, token, file, { notebookId: localId, inputs: createOptions.inputs }, createOptions, project
      )
      return { ...createdTarget, created: true }
    }
  )
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/local-runner/src/schedule-in-cloud.ts` around lines 77 - 115,
Extract the duplicated find-or-create flow into a shared
resolveOrCreateCloudNotebook helper. In
packages/local-runner/src/schedule-in-cloud.ts lines 77-115, replace the inline
coordination with this helper using inputs: {} and the schedule progress/warning
callbacks; in packages/local-runner/src/run-in-cloud.ts lines 142-180, replace
its equivalent block using the run inputs and full creation options. Preserve
coordination, notebook lookup, allowCreate/not-found handling, project lookup,
and createFromFile behavior, and update both callers to use the shared helper.

Source: Learnings

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/local-runner/src/schedule-in-cloud.ts`:
- Around line 77-115: Extract the duplicated find-or-create flow into a shared
resolveOrCreateCloudNotebook helper. In
packages/local-runner/src/schedule-in-cloud.ts lines 77-115, replace the inline
coordination with this helper using inputs: {} and the schedule progress/warning
callbacks; in packages/local-runner/src/run-in-cloud.ts lines 142-180, replace
its equivalent block using the run inputs and full creation options. Preserve
coordination, notebook lookup, allowCreate/not-found handling, project lookup,
and createFromFile behavior, and update both callers to use the shared helper.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bd2c1304-87ec-4bd1-b575-be44d44734b3

📥 Commits

Reviewing files that changed from the base of the PR and between 0e78fe7 and ba4c2be.

📒 Files selected for processing (9)
  • examples/local-runner/run-app/README.md
  • examples/local-runner/run-app/index.html
  • packages/local-runner/README.md
  • packages/local-runner/src/cloud-notebook-coordinator.test.ts
  • packages/local-runner/src/cloud-notebook-coordinator.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/schedule-in-cloud.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/serve-static.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/local-runner/run-app/README.md
  • packages/local-runner/README.md

coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 30, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/local-runner/src/recurring-schedule.test.ts`:
- Around line 13-26: Expand the parameterized tests around
resolveRecurringSchedule to cover array-form schedules, valid time boundaries
00:00 and 23:59, weekday values 0 and 6, month-day values 1 and 31, plus
malformed inputs containing special characters or whitespace. Add assertions for
the exact resolved schedule output on valid boundary cases while preserving the
existing RecurringScheduleError checks for invalid inputs.

In `@packages/local-runner/src/recurring-schedule.ts`:
- Around line 43-50: Update the monthly validation in the recurring schedule
handling around the frequency check to reject dayOfMonth values 29–31, since the
current cron expression skips shorter months. Extend the existing
RecurringScheduleError validation while preserving valid values 1–28 and the
current cron and description behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: e5c28141-0db0-4c5c-beda-013cc68f5266

📥 Commits

Reviewing files that changed from the base of the PR and between ba4c2be and 3e3bf80.

📒 Files selected for processing (8)
  • examples/local-runner/run-app/README.md
  • examples/local-runner/run-app/index.html
  • packages/local-runner/README.md
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/recurring-schedule.test.ts
  • packages/local-runner/src/recurring-schedule.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/serve-static.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • examples/local-runner/run-app/README.md
  • packages/local-runner/README.md

Comment thread packages/local-runner/src/recurring-schedule.test.ts
Comment thread packages/local-runner/src/recurring-schedule.ts

@dinohamzic dinohamzic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

AI review:

[P1] Preserve init-notebook semantics

packages/local-runner/src/schedule-in-cloud.ts:102

The create-if-missing path creates every notebook but never preserves file.project.initNotebookId. Deepnote’s public creation API leaves init_notebook_id null, while scheduled execution prepends init only when that field is set.

An init-backed file therefore schedules successfully but can fail on a cold run without its setup notebook. Preserve the designation through an import/API path, or reject this case before creating a broken schedule.

[P1] Share only neutral creation state

packages/local-runner/src/cloud-notebook-coordinator.ts:38

The coordinator coalesces the first caller’s entire resolver while its key excludes caller-specific inputs and block IDs.

If a run wins the race, createFromFile persists its one-off input overrides and the concurrent schedule inherits those values as recurring defaults. If scheduling wins, the file defaults are persisted. Invalid run-only block IDs can likewise reject an otherwise-valid concurrent schedule.

Prevalidate run-only options and coordinate a neutral, source-default creation operation.

[P1] Do not trust Host for same-origin validation

packages/local-runner/src/serve-static.ts:262

Host is client-controlled, making this check vulnerable to DNS rebinding. Once an attacker hostname resolves to 127.0.0.1, the browser sends matching attacker-controlled Origin and Host values, allowing the token-backed scheduling mutation.

Validate the parsed origin against the actual loopback listener address and port, or require an unguessable CSRF token.

[P2] Reject ambiguous sibling mappings

packages/local-runner/src/run-in-cloud.ts:400

Each local sibling is mapped to the first cloud notebook with the same name. The .deepnote schema permits duplicate notebook names, so two local helpers named Helper both map to one cloud ID. A notebook-function targeting the second helper can then silently execute whichever cloud helper exists.

Detect ambiguous referenced names and fail closed, as the fresh-project path already does for duplicate names.

[P2] Select a project that accepts additional notebooks

packages/cloud/src/projects.ts:142

The list API returns projectType, but the parser discards it and findProject blindly selects the newest exact-name project.

Deepnote-internal rejects notebook creation in single-notebook and Agent projects. One of those projects can therefore shadow an older usable standard project and make create-if-missing fail with HTTP 409.

Preserve projectType and select a standard project as the destination.

[P2] Resolve composed init projects consistently

packages/cli/src/commands/schedule.ts:104

Without --notebook, the CLI rejects every two-notebook file as ambiguous, including the supported [init, main] shape with exactly one runnable main notebook.

resolveSnapshotNotebookId already resolves this shape consistently elsewhere. Pass the project/init metadata through that helper and add an init-backed CLI test.

[P2] Classify malformed files as usage errors

packages/cli/src/commands/schedule.ts:40

deserializeDeepnoteFile throws ParseError subclasses for malformed YAML or schema, but this classification omits them and exits with code 1.

The new schedule documentation promises exit code 2 for invalid files, and neighboring CLI commands already treat ParseError as invalid usage. Add it here and test a malformed file.

[P2] Align first-schedule safety defaults

packages/cli/src/commands/schedule.ts:72

The internal API accepts only cron and timezone. Its create branch supplies neither republish nor autoPauseOnFailure, so database defaults leave both disabled.

The product UI defaults both settings to enabled. Consequently, CLI-created schedules do not refresh an existing published app and continue running after repeated failures.

Align the internal API’s creation defaults or expose these settings in the request contract before shipping the CLI.

[P2] Avoid concentrating hourly jobs at minute zero

packages/cli/src/utils/schedule-expression.ts:76

--hourly always emits minute zero and cannot accept --at. Deepnote-internal deliberately defaults scheduling to the current minute to avoid full-hour execution spikes.

Preserve or distribute the creation minute, or provide an hourly-minute option, rather than aligning every CLI-created hourly schedule.

Cross-reference note

Monthly days 1–31 match Deepnote’s internal scheduling UI. CodeRabbit’s suggested 1–28 restriction should not be adopted.

jamesbhobbs and others added 5 commits August 1, 2026 18:21
The same-origin check compared Origin to the Host header. Both are client
controlled, so matching them only proves the caller sent itself a matching
pair — which is what a DNS-rebinding page does once its hostname resolves to
127.0.0.1, carrying this server's cloud token off the back of it.

Check the Origin against the socket the request arrived on instead: a loopback
hostname, and the port from req.socket.localPort rather than a claimed one.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A run and a schedule racing for the same missing notebook resolve through one
create. That create baked in the first caller's input overrides, so a schedule
that joined a run inherited that run's one-off arguments as its recurring
defaults, and a run-only blockIds typo failed whichever schedule joined it.

Create the file as it stands. A run still executes with its overrides — it
passes them to triggerNotebookRun, where they belong to the run rather than to
the notebook. Validate blockIds before entering the coordinator, so a bad
argument fails only the call that made it.

Also refuse an ambiguous sibling mapping: the schema permits duplicate notebook
names, and matching by name pointed a notebook-function call at whichever cloud
notebook came first.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…it notebook

A scheduled run prepends the project's init notebook only when Deepnote has
init_notebook_id set, and the public creation API has no field for it. An
init-backed file therefore scheduled successfully and then ran without its
setup at whatever hour the cron named — the least visible place to find out.

Refuse on the create path with a way through: import the project once, which
keeps the designation, then schedule it. A project already in Deepnote is
unaffected.

Also cover the recurring-schedule parser's boundaries: array input, 00:00 and
23:59, weekday 0 and 6, day 1 and 31, and malformed times carrying cron
metacharacters, whitespace, or a trailing expression.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… files

Three fixes to the schedule command:

--hourly always emitted minute zero and refused --at, so every CLI-created
hourly schedule landed on the same execution spike. Use the creation minute by
default, and let --at pin one (":15", "15", or an HH:mm whose hour is 00).

Without --notebook, every two-notebook file was rejected as ambiguous — including
the composed [init, main] shape, which has exactly one runnable notebook and is
resolved consistently everywhere else. Route it through
resolveSnapshotNotebookId, and keep the refusal for files it cannot resolve.

A malformed or schema-invalid file exited 1, while the command's own help text
promises 2 for a bad file. Classify ParseError as invalid usage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jamesbhobbs

Copy link
Copy Markdown
Contributor Author

@dinohamzic thanks — worked through all nine. Seven are fixed in b166daf..1cc9009; two need your input because they depend on things outside this repo.

Fixed

[P1] Preserve init-notebook semanticscreateProject has no init_notebook_id field, so preserving the designation through the public API is not available. Took the other option and refused before creating a broken schedule: scheduleInCloud now throws when the file declares an initNotebookId that resolves to a real notebook and the project is not in Deepnote yet, pointing at the way through (import once, which keeps the designation, then schedule). A project already in Deepnote is unaffected, since nothing is created. Refused before the first POST, so there is no half-built project left behind.

[P1] Share only neutral creation state — took the neutral-create route. createFromFile no longer bakes input overrides into what it writes; a run passes its overrides to triggerNotebookRun, where they belong to the run rather than to the notebook. That keeps the shared create genuinely shareable, so a run joining a schedule-triggered create still gets the block-id mapping — which keying the coordinator on caller state would have cost. Run-only blockIds are now validated before entering the coordinator, so a typo fails only the call that made it rather than whichever schedule joined it.

Two existing tests asserted the old baked-in behaviour and were inverted deliberately — worth a look, since that was a documented design choice I have changed: run-in-cloud.test.ts "creates blocks in sortingKey order…" and "creates a not-found multi-notebook file from its own values…". The coordinator test now also pins that the shared create holds the file's value, not the racing run's.

[P1] Do not trust Host — right, matching two client-controlled headers proved nothing. The Origin is now checked against the socket the request arrived on: a loopback hostname, and the port from req.socket.localPort. Added a raw-http.request test sending the DNS-rebinding shape (matching attacker Origin and Host) — fetch refuses to set Host, which is why it bypasses it — plus a loopback-origin-on-another-port case and a localhost/127.0.0.1 spelling check.

[P2] Reject ambiguous sibling mappings — fails closed now. If a notebook-function block names a local sibling whose name is shared with another notebook in the file, it throws rather than mapping both onto the first cloud match.

[P2] Resolve composed init projects consistently — routed through resolveSnapshotNotebookId, so [init, main] resolves to the main notebook without --notebook. The ambiguity refusal stays for files it cannot resolve. Added a CLI test with an init-backed file.

[P2] Classify malformed files as usage errorsParseError now maps to exit 2, matching the help text and the other file commands. Tests for malformed YAML and a schema-invalid file.

[P2] Avoid concentrating hourly jobs at minute zero--hourly now uses the creation minute, and --at is accepted with it to pin one (:15, 15, or an HH:mm whose hour is 00; a non-zero hour is refused rather than silently dropped). resolveScheduleExpression takes an injectable now so this stays testable. Docs updated in the CLI README and the schedule skill reference.

Needs your input

[P2] Select a project that accepts additional notebooks — I can preserve projectType through the parser and prefer a standard project in findProject, but projectType appears nowhere in this repo and the list API's values are not in anything I can see here. What are the identifiers for the standard, single-notebook, and Agent types? With those I will add it. Guessing the enum seemed worse than leaving findProject honest about picking the newest exact-name match.

[P2] Align first-schedule safety defaults — agreed on the substance, but both halves land outside this PR: UpsertNotebookScheduleBody accepts only cron and timezone, so exposing republish/autoPauseOnFailure means changing the internal API's request contract first, and aligning its creation defaults is a deepnote-internal change. Happy to add the fields here the moment the contract carries them — do you want that tracked as a follow-up, or should this PR stay draft until the internal side lands?

CI is green and the full suite passes (2764 tests). Leaving it in draft.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
packages/cli/src/utils/schedule-expression.ts (1)

71-78: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Validate cron field syntax before scheduling.

Line 76 only checks the field count. A value such as invalid foo bar baz qux passes and reaches scheduleInCloud.

Validate each field with the same cron grammar that Deepnote Cloud accepts. Add invalid-token tests.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/utils/schedule-expression.ts` around lines 71 - 78, Update
the cron validation in the options.cron handling path to validate each of the
five fields against the cron grammar accepted by Deepnote Cloud, not only the
field count. Reject invalid tokens before scheduleInCloud is reached, preserve
the existing empty and incorrect-count errors, and add tests covering malformed
cron fields such as “invalid foo bar baz qux”.
packages/local-runner/src/serve-static.ts (1)

175-176: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Apply the origin guard to the other token-backed route.

rejectCrossOriginRequest protects /api/schedule-cloud only. POST /api/run-cloud at Line 153 also uses options.cloudToken and mutates Cloud state. A page on another origin can still reach it over loopback, with the same DNS-rebinding path this guard was added to close.

Call the guard for /api/run-cloud as well, or place it once before the route dispatch for all POST handlers.

Proposed fix
     if (req.method === 'POST' && pathname === '/api/run-cloud') {
+      if (rejectCrossOriginRequest(req, res)) return
       const body = await readJsonBody(req, res)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/local-runner/src/serve-static.ts` around lines 175 - 176, Apply
rejectCrossOriginRequest to the POST /api/run-cloud handler as well as
/api/schedule-cloud, or move the guard to a shared location covering every POST
route before dispatch. Preserve existing route behavior while ensuring both
token-backed Cloud mutation endpoints reject cross-origin requests.
packages/local-runner/src/schedule-in-cloud.ts (1)

99-114: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Move the init check behind the project lookup.

The doc comment states that only the create path loses the init designation. An existing Cloud project keeps its designation, and addNotebookToExistingProject does not touch it. But the assertion runs before findProject, so a file with initNotebookId is refused even when the project already exists in Deepnote and only the notebook is missing. That path is safe, and the error text ("Import the project into Deepnote first") then names an action the user already did.

Run the assertion only when no project is found.

Proposed fix
-        assertInitNotebookSurvivesCreation(file)
-
         // A matching project can exist without this notebook. Add to it rather than creating a
         // duplicate project; the coordinator serializes this check with cloud runs and schedules.
         const project = await findProject(baseUrl, token, file.project.name)
+        if (!project) {
+          assertInitNotebookSurvivesCreation(file)
+        }
         const createdTarget = await createFromFile(

Add a test for an existing project with a missing notebook in an init-backed file.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/local-runner/src/schedule-in-cloud.ts` around lines 99 - 114, Move
the assertInitNotebookSurvivesCreation call to after findProject and invoke it
only when no existing project is found, preserving the safe existing-project
path through createFromFile. Add a test covering an init-backed file whose
project exists but whose notebook is missing, verifying the notebook is added
without triggering the assertion.
packages/local-runner/src/run-in-cloud.ts (1)

340-364: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Building specs for every notebook wastes work and can emit unrelated warnings.

allNotebookSpecs maps over toCreate.project.notebooks and runs toBlockSpec(block, options.onWarning) for every notebook's blocks, unconditionally. When destination is set, only allNotebookSpecs[index] (the target notebook) is used in addNotebookToExistingProject; the specs for every other notebook in the file are still built. If toBlockSpec emits a warning for problematic metadata (per the retrieved learning about dropping invalid metadata with a warning), a user adding one notebook to an existing project can see warnings about blocks in unrelated sibling notebooks that are not being touched by this operation. This is misleading and wastes computation.

Build the full allNotebookSpecs array only for the createProject (no-destination) path, and compute a spec for just the target notebook when destination is set.

🐛 Proposed fix to scope spec building to the target notebook when extending an existing project
-  const allNotebookSpecs: ProjectSpec['notebooks'] = toCreate.project.notebooks.map((notebook, i) => ({
-    // The file's own id for this notebook, so `rewriteBlock` below can turn a block's reference to
-    // it into the id Deepnote assigns.
-    sourceId: notebook.id,
-    name: notebook.name,
-    blocks: sortedBlocks[i].map(block => toBlockSpec(block, options.onWarning)),
-  }))
-  const spec: ProjectSpec = {
-    name: toCreate.project.name,
-    notebooks: allNotebookSpecs,
-  }
-
   const createOptions = {
     onProgress: options.onCreateProgress,
     onWarning: options.onWarning,
     rewriteBlock: rewriteNotebookFunctionId,
   }

-  const result = destination
-    ? await addNotebookToExistingProject(baseUrl, token, destination, toCreate, index, allNotebookSpecs[index], {
-        ...createOptions,
-      })
-    : await createProject(baseUrl, token, spec, createOptions)
-
-  const match = result.notebooks[destination ? 0 : index]
+  const toNotebookSpec = (notebook: (typeof toCreate.project.notebooks)[number], i: number): ProjectSpec['notebooks'][number] => ({
+    // The file's own id for this notebook, so `rewriteBlock` below can turn a block's reference to
+    // it into the id Deepnote assigns.
+    sourceId: notebook.id,
+    name: notebook.name,
+    blocks: sortedBlocks[i].map(block => toBlockSpec(block, options.onWarning)),
+  })
+
+  const result = destination
+    ? await addNotebookToExistingProject(
+        baseUrl,
+        token,
+        destination,
+        toCreate,
+        index,
+        toNotebookSpec(toCreate.project.notebooks[index], index),
+        { ...createOptions }
+      )
+    : await createProject(
+        baseUrl,
+        token,
+        { name: toCreate.project.name, notebooks: toCreate.project.notebooks.map(toNotebookSpec) },
+        createOptions
+      )
+
+  const match = result.notebooks[destination ? 0 : index]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/local-runner/src/run-in-cloud.ts` around lines 340 - 364, Update the
spec-building flow around allNotebookSpecs so destination mode converts only
toCreate.project.notebooks[index] via toBlockSpec and passes that single target
spec to addNotebookToExistingProject. Build the complete allNotebookSpecs array
only in the createProject path, preserving full-project conversion and warnings
when destination is unset.

Source: Learnings

🧹 Nitpick comments (1)
packages/cli/src/commands/schedule.test.ts (1)

83-85: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Drop the unchecked cast.

tempDirs.pop() as string hides the undefined case from the type checker. Iterate the drained array instead.

Proposed refactor
-    while (tempDirs.length > 0) {
-      rmSync(tempDirs.pop() as string, { recursive: true, force: true })
-    }
+    for (const dir of tempDirs.splice(0)) {
+      rmSync(dir, { recursive: true, force: true })
+    }

As per coding guidelines: "prefer type safety over convenience".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/commands/schedule.test.ts` around lines 83 - 85, Update the
temporary-directory cleanup loop to avoid the unchecked string cast on
tempDirs.pop(). Drain and iterate the array using a type-safe approach that
handles the empty/undefined case while preserving recursive forced removal for
each directory.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/utils/schedule-expression.test.ts`:
- Around line 6-8: Add exact-output test cases to the hourly schedule-expression
cases for parseHourlyMinute boundary inputs `--at :00` and `--at :59`, asserting
they produce `0 * * * *` and `59 * * * *` respectively while preserving the
existing test structure and descriptions.

In `@packages/local-runner/src/run-in-cloud.ts`:
- Around line 172-178: Update the runInCloud creation flow around createFromFile
to perform the same pre-creation init-notebook existence assertion used by
scheduleInCloud, preserving file.project.initNotebookId before creating content.
Ensure the not-found case fails before createFromFile runs, and add a regression
test covering that path.

---

Outside diff comments:
In `@packages/cli/src/utils/schedule-expression.ts`:
- Around line 71-78: Update the cron validation in the options.cron handling
path to validate each of the five fields against the cron grammar accepted by
Deepnote Cloud, not only the field count. Reject invalid tokens before
scheduleInCloud is reached, preserve the existing empty and incorrect-count
errors, and add tests covering malformed cron fields such as “invalid foo bar
baz qux”.

In `@packages/local-runner/src/run-in-cloud.ts`:
- Around line 340-364: Update the spec-building flow around allNotebookSpecs so
destination mode converts only toCreate.project.notebooks[index] via toBlockSpec
and passes that single target spec to addNotebookToExistingProject. Build the
complete allNotebookSpecs array only in the createProject path, preserving
full-project conversion and warnings when destination is unset.

In `@packages/local-runner/src/schedule-in-cloud.ts`:
- Around line 99-114: Move the assertInitNotebookSurvivesCreation call to after
findProject and invoke it only when no existing project is found, preserving the
safe existing-project path through createFromFile. Add a test covering an
init-backed file whose project exists but whose notebook is missing, verifying
the notebook is added without triggering the assertion.

In `@packages/local-runner/src/serve-static.ts`:
- Around line 175-176: Apply rejectCrossOriginRequest to the POST /api/run-cloud
handler as well as /api/schedule-cloud, or move the guard to a shared location
covering every POST route before dispatch. Preserve existing route behavior
while ensuring both token-backed Cloud mutation endpoints reject cross-origin
requests.

---

Nitpick comments:
In `@packages/cli/src/commands/schedule.test.ts`:
- Around line 83-85: Update the temporary-directory cleanup loop to avoid the
unchecked string cast on tempDirs.pop(). Drain and iterate the array using a
type-safe approach that handles the empty/undefined case while preserving
recursive forced removal for each directory.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19c156b1-f4d5-49d2-8303-7583924e04ec

📥 Commits

Reviewing files that changed from the base of the PR and between 3e3bf80 and 1cc9009.

📒 Files selected for processing (17)
  • packages/cli/README.md
  • packages/cli/src/cli.ts
  • packages/cli/src/commands/schedule.test.ts
  • packages/cli/src/commands/schedule.ts
  • packages/cli/src/utils/schedule-expression.test.ts
  • packages/cli/src/utils/schedule-expression.ts
  • packages/local-runner/README.md
  • packages/local-runner/src/cloud-notebook-coordinator.test.ts
  • packages/local-runner/src/cloud-notebook-coordinator.ts
  • packages/local-runner/src/recurring-schedule.test.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/schedule-in-cloud.test.ts
  • packages/local-runner/src/schedule-in-cloud.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/serve-static.ts
  • skills/deepnote/references/cli-schedule.md
🚧 Files skipped from review as they are similar to previous changes (7)
  • packages/cli/README.md
  • skills/deepnote/references/cli-schedule.md
  • packages/cli/src/cli.ts
  • packages/local-runner/src/cloud-notebook-coordinator.test.ts
  • packages/local-runner/README.md
  • packages/local-runner/src/cloud-notebook-coordinator.ts
  • packages/cli/src/commands/schedule.ts

Comment thread packages/cli/src/utils/schedule-expression.test.ts
Comment thread packages/local-runner/src/run-in-cloud.ts
findProject returned the newest exact-name project regardless of its type. The
public spec types projects as standard | notebook | agent, and Deepnote rejects
notebook creation in the latter two — so a newer single-notebook or Agent
project shadowed an older usable one and turned createIfMissing into HTTP 409.

Preserve projectType through the parser and skip the closed types. When every
match is closed, report no destination: the caller then creates a fresh
standard project, which is the outcome that works. An unreported type stays
usable, since refusing what we cannot read would bring back the duplicate
projects this lookup exists to prevent.

Enum values from the public OpenAPI spec at
https://api.deepnote.com/v2/openapi.json.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jamesbhobbs

Copy link
Copy Markdown
Contributor Author

Correcting my previous comment on the two "needs input" items — I had only searched this repo, not the public API docs. The spec at https://api.deepnote.com/v2/openapi.json answers both.

[P2] Select a project that accepts additional notebooks — now fixed in 336d971. No input needed after all; the enum is public:

"projectType": {
  "description": "Project type. Omit to create a `standard` project; `notebook` creates a single-notebook project; `agent` creates an Agent project.",
  "type": "string",
  "enum": ["standard", "notebook", "agent"]
}

It is a required field on the project response. projectSchema now preserves it and findProject skips notebook and agent, exactly as you described. Three details worth confirming:

  • When every exact-name match is closed, findProject returns undefined rather than a project the create would 409 on — the caller then makes a fresh standard project. That can leave two same-named projects (one an Agent project), which seemed clearly better than a hard failure, but say if you'd rather it threw.
  • An unreported type is treated as usable. Refusing what we cannot read would bring back the duplicate projects this lookup exists to prevent, so the unknown case fails in the safe direction.
  • Kept as z.string() rather than a zod enum, so a fourth project type ships without breaking every lookup in this client.

[P2] Align first-schedule safety defaults — confirmed out of scope for this repo, with stronger evidence than I had. UpsertNotebookScheduleBody is:

{
  "type": "object",
  "properties": { "cron": {...}, "timezone": { "default": "UTC", ... } },
  "required": ["cron"],
  "additionalProperties": false
}

additionalProperties: false means sending republish or autoPauseOnFailure today would be rejected, not ignored — so this is blocked on the internal contract carrying them, and on that side's creation defaults. The gap is now written down in the schedule skill reference rather than left implicit, so a CLI-created schedule not matching the UI's defaults is at least documented. Still happy to wire the fields the moment the contract has them — follow-up issue, or hold this PR?

Full suite green (2767 tests). Still draft.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cloud/src/projects.test.ts`:
- Around line 237-245: Add a test alongside the existing untyped-project case
that returns a project with an unrecognized projectType such as "future-type",
then verify findProject still returns that project as usable. Keep the same
fetch mock and expected project result pattern, while ensuring the unknown value
is explicitly present.

In `@skills/deepnote/references/cli-schedule.md`:
- Around line 63-65: Update the project-selection documentation around the
`notebook` and `agent` exclusions to state that those known types are skipped
while unknown project types remain eligible. Align the wording with the
implementation rather than claiming that only `standard` projects can accept a
new notebook.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 6af98cd5-4c2e-4d97-b015-d586a0b1312a

📥 Commits

Reviewing files that changed from the base of the PR and between 1cc9009 and 336d971.

📒 Files selected for processing (3)
  • packages/cloud/src/projects.test.ts
  • packages/cloud/src/projects.ts
  • skills/deepnote/references/cli-schedule.md

Comment thread packages/cloud/src/projects.test.ts
Comment thread skills/deepnote/references/cli-schedule.md Outdated
… to new projects

Three fixes from review:

The origin guard covered /api/schedule-cloud only. POST /api/run-cloud spends
the same cloud token and creates project content when the notebook is not in
Deepnote yet, so the DNS-rebinding path the guard was added to close was still
open on it.

The init-notebook refusal ran before findProject, so an init-backed file was
refused even when its project was already in Deepnote and only the notebook was
missing — a path that loses nothing, and whose error told the user to import a
project they had already imported. Moved into createFromFile behind the
create-a-new-project branch, which also extends it to runInCloud, whose create
path drops the designation just as silently.

Building specs for every notebook when only one is being added meant toBlockSpec
warned about blocks in siblings the operation never touches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jamesbhobbs

Copy link
Copy Markdown
Contributor Author

@coderabbitai the four outside-diff-range findings, addressed in 4d64e0e:

serve-static.ts — apply the origin guard to the other token-backed route. Right, and thanks for catching it. POST /api/run-cloud spends the same cloud token and creates project content when the notebook is not in Deepnote yet, so hardening only the schedule route left the same DNS-rebinding door open next to it. Guard applied, with a test asserting a foreign origin gets 403 there too.

schedule-in-cloud.ts — move the init check behind the project lookup. Correct, and the error text was actively misleading: it told the user to import a project they had already imported. Fixed by moving the assertion into createFromFile behind the !destination branch, which is the create-a-new-project path and nothing else. That also resolves the inline run-in-cloud.ts finding in the same place rather than duplicating the check.

run-in-cloud.ts — building specs for every notebook. Agreed, and the misleading-warnings half is the real cost: toBlockSpec warns as it goes, so adding one notebook could report problems in siblings the operation never touches. Spec building is now per-notebook via a specFor(i) helper — the full set only for createProject, just the target for addNotebookToExistingProject. Test asserts one spec goes over the wire.

schedule-expression.ts — validate cron field syntax. Not taking this one. --cron "invalid foo bar baz qux" does reach the API, but the API validates it and returns HTTP 400, which this command already maps to exit code 2 with the server's message — so the user gets a clear, correctly-classified failure either way. Writing a local cron parser to duplicate that would be a heavy lift, as you note, and the failure mode of getting it wrong is worse than the problem: cron legitimately accepts JAN-DEC/MON-SUN names, step values, ranges, and lists, so a hand-rolled validator that is even slightly too strict would reject expressions Deepnote accepts. The server is authoritative on its own cron dialect and I would rather defer to it than shadow it. Happy to revisit if there is a spec-published grammar to validate against.

Full suite green (2773 tests).

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

git show --no-ext-diff --stat --oneline 4d64e0e
git show --no-ext-diff --format= -- \
  packages/local-runner/src/serve-static.ts \
  packages/local-runner/src/serve-static.test.ts \
  packages/local-runner/src/schedule-in-cloud.ts \
  packages/local-runner/src/run-in-cloud.ts \
  packages/local-runner/src/run-in-cloud.test.ts \
  packages/cli/src/commands/schedule.ts \
  packages/cli/src/utils/schedule-expression.ts \
  4d64e0e

printf '\n--- outlines ---\n'
ast-grep outline packages/local-runner/src/serve-static.ts --items all --type function
ast-grep outline packages/local-runner/src/run-in-cloud.ts --items all --type function
ast-grep outline packages/local-runner/src/schedule-in-cloud.ts --items all --type function

printf '\n--- targeted references ---\n'
rg -n -C 4 \
  'run-cloud|schedule-cloud|Origin|localPort|createFromFile|initNotebookId|specFor|toBlockSpec|statusCode|ScheduleExpressionError|ApiError' \
  packages/local-runner/src/serve-static.ts \
  packages/local-runner/src/serve-static.test.ts \
  packages/local-runner/src/schedule-in-cloud.ts \
  packages/local-runner/src/run-in-cloud.ts \
  packages/local-runner/src/run-in-cloud.test.ts \
  packages/cli/src/commands/schedule.ts \
  packages/cli/src/utils/schedule-expression.ts

Length of output: 73630


@jamesbhobbs Verified in 4d64e0e.

  • POST /api/run-cloud now uses the socket-based origin guard before it reads the body. The regression test expects HTTP 403 for a foreign origin.
  • assertInitNotebookSurvivesCreation now runs only for new-project creation. Existing projects can receive the missing notebook without the misleading refusal.
  • The existing-project path now builds only the target notebook spec. The test confirms that one spec reaches addNotebooksToProject.

I also withdraw the cron field-validation finding. The CLI validates empty and non-five-field cron input. The API validates its authoritative cron dialect. HTTP 400 maps to ExitCode.InvalidUsage, so the user receives the API validation message with the correct exit code.


✏️ Learnings added
Learnt from: jamesbhobbs
URL: https://github.com/deepnote/deepnote/pull/437

Timestamp: 2026-08-01T23:58:30.680Z
Learning: For `deepnote schedule`, `packages/cli/src/utils/schedule-expression.ts` performs only structural cron validation, while the Deepnote API is authoritative for cron field syntax and dialect. `packages/cli/src/commands/schedule.ts` maps API HTTP 400 responses to `ExitCode.InvalidUsage`, so server-side cron validation errors are reported as CLI usage errors.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

You are interacting with an AI system.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/local-runner/src/schedule-in-cloud.test.ts`:
- Around line 286-288: Update the test around the second upsertNotebookSchedule
invocation to assert that it receives the scheduled notebook ID
`notebook-created`, in addition to the existing result and call-count
assertions. Use the mock’s recorded calls to verify the second call’s notebook
ID while preserving the current expectations.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19f470e7-5c9e-4d66-bc6e-7536cac50b9d

📥 Commits

Reviewing files that changed from the base of the PR and between 336d971 and 4d64e0e.

📒 Files selected for processing (7)
  • packages/cli/src/utils/schedule-expression.test.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/schedule-in-cloud.test.ts
  • packages/local-runner/src/schedule-in-cloud.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/serve-static.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/cli/src/utils/schedule-expression.test.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/schedule-in-cloud.ts
  • packages/local-runner/src/run-in-cloud.ts

Comment thread packages/local-runner/src/schedule-in-cloud.test.ts Outdated
findProject skips only the types known to reject notebook creation, so a type
this client has not heard of stays eligible. That was deliberate but only the
absent-field case was covered, leaving a later restrictive change free to break
it silently. The schedule reference said only standard projects qualify, which
did not match.

Also assert the retried schedule names the newly created notebook, rather than
trusting the call count.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Aug 2, 2026
@jamesbhobbs
jamesbhobbs requested a review from dinohamzic August 2, 2026 00:26
@jamesbhobbs
jamesbhobbs marked this pull request as ready for review August 3, 2026 12:27
@jamesbhobbs
jamesbhobbs requested a review from a team as a code owner August 3, 2026 12:27

@dinohamzic dinohamzic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Second round, not much left (I'm staring manual testing next):

[P1] Do not treat an unresolved sibling init notebook as absent

packages/local-runner/src/run-in-cloud.ts:457-460

When initNotebookId references a notebook outside the current file, this branch returns successfully and creation continues without the init notebook.

This is a supported file shape: splitByNotebooks deliberately preserves initNotebookId in each main file so the sibling resolver can find the standalone init file (packages/convert/src/snapshot/split.test.ts:654-660). However, the schedule command directly deserializes the selected file (packages/cli/src/commands/schedule.ts:62-65) and never calls resolveAndComposeInitIfNeeded.

Consequently, scheduling a split main file can create and schedule it without its required setup. Please resolve sibling init notebooks before scheduling, or fail closed whenever a declared init ID cannot be resolved.

[P1] An exact-name destination does not prove the init setup is present

packages/local-runner/src/run-in-cloud.ts:339-341

The init-preservation check is skipped whenever findProject returns an existing exact-name project. The public project response does not expose whether that project has an init designation, and only the target notebook is uploaded.

The new test at packages/local-runner/src/schedule-in-cloud.test.ts:272-298 demonstrates the unsafe case: it accepts an existing project with notebooks: [] and schedules the newly added main notebook. Internally, scheduled execution prepends setup only when project.init_notebook_id is set (deepnote-internal/apps/webapp/server/modules/scheduling/run-scheduled-executions.ts:574-579).

Therefore, an unrelated same-name project—or one whose init designation was removed—will run the scheduled notebook without setup. Please fail closed unless the API can verify or establish the destination’s init designation.

[P2] Spread daily, weekly, and monthly defaults too

packages/cli/src/utils/schedule-expression.ts:97

Hourly schedules now use the creation minute, but every daily, weekly, and monthly schedule without --at still defaults to 09:00.

The internal scheduler intentionally defaults new schedules to the current hour and minute to avoid execution spikes (deepnote-internal/apps/webapp-client/src/notebook/features/scheduling/containers/SchedulingModalContainer.tsx:72-74). The CLI should use the same strategy for all generated schedules, not only hourly ones.

[P2] Match the UI’s first-schedule defaults

packages/cloud/src/schedules.ts:27-32

The client can send only cron and timezone. The internal strict request schema has the same limitation, and the create path consequently relies on database defaults:

  • republish = false
  • auto_pause_on_failure = false

The product UI defaults both settings to true (SchedulingModalContainer.tsx:182-190). Thus, creating the first schedule through the CLI silently behaves differently from creating it through Deepnote: published apps are not republished and repeatedly failing schedules are not automatically paused.

Please extend the internal endpoint and client to apply the product defaults when creating a schedule. Existing schedule updates should continue preserving the user’s configured values.

Confirmed fixed

The latest commits correctly address:

  • caller-specific inputs leaking into shared project creation;
  • target block-ID isolation;
  • ambiguous sibling notebook-function mappings;
  • project-type selection;
  • invalid-file exit-code handling;
  • hourly minute spreading and explicit --at;
  • monthly days 1–31;
  • socket-based origin validation for the cloud mutation routes.

…dule default

Two P1s from review, which collapse into one rule: creating content for a file
that declares an initNotebookId is never safe, so refuse whenever it is set.

An id that resolves to nothing in the file is not an absent init.
splitByNotebooks deliberately keeps initNotebookId in every main file so the
sibling resolver can find the standalone init file, and the schedule command
deserializes the selected file without composing it — so the previous "names
nothing here, nothing to lose" branch scheduled split main files without their
setup. Composing would not have helped either: the composed [init, main] still
needs a designation this API cannot set.

An existing exact-name project is not evidence either. Only the target notebook
is uploaded into it, and the API will not report whether that project carries an
init designation, so an unrelated project sharing the name runs the notebook
without setup just the same. The destination gate is gone.

Separately, only hourly schedules were being spread. Deepnote's scheduling UI
defaults every new schedule to the current hour and minute for the same reason,
so daily, weekly and monthly now use the creation time instead of 09:00.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/utils/schedule-expression.test.ts`:
- Around line 24-28: Update the test “defaults to the system timezone” to
compare result.timezone from resolveScheduleExpression({ daily: true }) against
the documented system-timezone value, replacing the non-specific toBeTruthy
assertion and preserving the existing test setup.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 935f4059-b600-46a6-8e28-43772b8aa2f7

📥 Commits

Reviewing files that changed from the base of the PR and between efc75ce and 2fdd3d8.

📒 Files selected for processing (9)
  • packages/cli/README.md
  • packages/cli/src/cli.ts
  • packages/cli/src/utils/schedule-expression.test.ts
  • packages/cli/src/utils/schedule-expression.ts
  • packages/local-runner/README.md
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/schedule-in-cloud.test.ts
  • skills/deepnote/references/cli-schedule.md
🚧 Files skipped from review as they are similar to previous changes (8)
  • skills/deepnote/references/cli-schedule.md
  • packages/cli/src/cli.ts
  • packages/cli/README.md
  • packages/local-runner/src/schedule-in-cloud.test.ts
  • packages/cli/src/utils/schedule-expression.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/README.md
  • packages/local-runner/src/run-in-cloud.test.ts

Comment thread packages/cli/src/utils/schedule-expression.test.ts
@jamesbhobbs

Copy link
Copy Markdown
Contributor Author

@dinohamzic second round handled — three fixed in 2fdd3d8, and the fourth verified but deliberately left to you.

[P1] Unresolved sibling init notebook — fixed

You're right, and the old branch was worse than "returns successfully": its comment asserted "the designation names nothing in this file, so there is no setup to lose", which is exactly the wrong reading of the split shape. Confirmed against split.test.ts:654-660initNotebookId is preserved in every main file precisely so the sibling resolver can find the standalone init.

Worth noting composing would not have fixed it either: resolveAndComposeInit produces [init, main] as two notebooks with initNotebookId still set, so the created project still needs a designation this API cannot set. Failing closed is the only sound option.

[P1] Exact-name destination proves nothing — fixed

Agreed, and this reverses a change I made last round on CodeRabbit's suggestion that the existing-project path was safe. Your reasoning wins: only the target notebook is uploaded, and the API will not report the destination's designation, so a same-name project — or one whose designation was removed — runs without setup.

Both P1s now collapse into one rule in createFromFile: if initNotebookId is set, refuse to create, full stop. No destination gate, no unresolvable-id escape. That covers runInCloud too, which had the same hole. Tests cover the new-project, existing-project, and sibling-init cases for both entry points.

[P2] Spread daily/weekly/monthly — fixed

Fair — spreading only hourly was half a fix. All cadences now default to the creation time, matching SchedulingModalContainer's defaultTime = dayjs().format('HH:mm') and the reasoning in its comment. 09:00 was arguably the worst possible fixed default. --at still pins a time; resolveScheduleExpression takes an injectable clock so it stays testable.

[P2] First-schedule safety defaults — verified, not actioned here

I verified your claim exhaustively against the published spec rather than assuming, and it holds in the strongest form:

  • UpsertNotebookScheduleBody is {cron, timezone} with additionalProperties: false — extra fields are rejected, not ignored.
  • Zero occurrences of republish, pause, failure, or consecutive anywhere in the 132 KB spec.
  • No write endpoint in the entire public API could carry them: POST /projects takes only folderId/name/projectType, and there is no project or notebook settings update endpoint.
  • Column defaults confirmed as you said (republish false, auto_pause_on_failure false), against max_consecutive_failures defaulting to 3, which already matches the modal.

So there is no way to close this from this repo — it needs the internal create branch to apply the product defaults, which is your call to sequence rather than something I should land unilaterally in a different repo. Flagging one thing for whoever picks it up: notebooks.schedule.test.ts's "preserves fields not included in an update, such as republish" seeds republish: true, which is the same value a defaulting create branch would produce — so it would keep passing even if update wrongly applied the create defaults. Seeding false would make it prove preservation.

Happy to wire the client side here the moment the contract carries the fields.


CI note: Test (Node 23) is the 5-minute job timeout hitting Build packages (5m16s, reported as cancelled, no test ever ran) — re-run requested. Audit - All is a newly published brace-expansion advisory; it passed on this branch ~19h ago on the identical lockfile and my commit touches no dependency files, so it is repo-wide rather than from this PR, and looks like a separate chore(security) bump.

@jamesbhobbs

Copy link
Copy Markdown
Contributor Author

CI follow-up: the Test (Node 23) red was the 5-minute job timeout as expected — it passes on re-run, so the only red left is Audit - All.

Refining what I said about that one: Audit - Production passes and only Audit - All fails, so the brace-expansion advisory sits in a dev-only dependency and nothing shipped is affected. Still repo-wide rather than from this PR (no dependency files touched here, and it passed on this branch ~19h ago on the identical lockfile), and still best handled as a separate chore(security) bump.

Everything else is green: 20/21 checks, full suite 2779 tests.

…y one

toBeTruthy() passed for any non-empty string, so the test would have gone on
passing if resolveTimezone stopped reading the system clock and hardcoded a
fallback. Compare against the zone Intl actually resolves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@jamesbhobbs
jamesbhobbs requested a review from dinohamzic August 3, 2026 19:39

@dinohamzic dinohamzic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Even when using --hourly to schedule via CLI, in Deepnote I then see "Cron (custom)" as the selected option instead of "Hourly".

It would be much more user friendly if --hourly, --daily, --weekly etc. matched the corresponding option in Deepnote Cloud.

I was not able to spot any other functional issues while manually testing, nice one.

Image

@jamesbhobbs

Copy link
Copy Markdown
Contributor Author

@coderabbitai open an issue describing this difference to deepnote cloud as something to fix later #437 (review)

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh api repos/deepnote/deepnote/pulls/comments/4853245965 \
  --jq '{id, html_url, body, path, line, user: .user.login}'

gh pr view 437 --repo deepnote/deepnote \
  --json url,title \
  --jq '{url,title}'

Length of output: 712


🏁 Script executed:

#!/bin/bash
set -euo pipefail

gh api repos/deepnote/deepnote/pulls/437/reviews/4853245965 \
  --jq '{id, html_url, body, user: .user.login}'

gh pr view 437 --repo deepnote/deepnote \
  --json url,title \
  --jq '{url,title}'

Length of output: 1114


🏁 Script executed:

#!/bin/bash
set -euo pipefail

ISSUE_TITLE="$(cat <<'EOF'
Preserve friendly schedule cadence for API-created notebook schedules
EOF
)"

ISSUE_BODY="$(cat <<'EOF'
## Summary

Notebook schedules created through the public API use a cron expression. When the CLI creates a schedule with `--hourly`, `--daily`, `--weekly`, or `--monthly`, Deepnote Cloud displays the schedule type as `Cron (custom)` instead of the matching friendly cadence.

Deepnote Cloud should preserve or recognize the friendly cadence for schedules created through the API.

## Rationale

The CLI presents friendly cadence options. The Deepnote Cloud UI should show the same cadence after the schedule is created. A `Cron (custom)` display makes a standard schedule appear different from the selected CLI option.

## Affected areas

- Deepnote Cloud notebook schedule creation and display
- Public schedule API contract
- Schedule persistence or schedule-type inference

## References

- PR: [`#437`](https://github.com/deepnote/deepnote/pull/437)
- Review: [pullrequestreview-4853245965](https://github.com/deepnote/deepnote/pull/437#pullrequestreview-4853245965)
- Requested by: `@jamesbhobbs`

## Acceptance criteria

- [ ] A schedule created with the API from an hourly cadence displays as `Hourly` in Deepnote Cloud.
- [ ] A schedule created with the API from daily, weekly, and monthly cadences displays as the matching cadence in Deepnote Cloud.
- [ ] A genuinely custom cron expression continues to display as `Cron (custom)`.
- [ ] The public API contract documents the supported behavior. If cadence metadata is required, the API accepts and returns it.
EOF
)"

issue_url="$(
  gh issue create \
    --repo deepnote/deepnote \
    --title "$ISSUE_TITLE" \
    --body "$ISSUE_BODY" \
    --assignee jamesbhobbs
)"

printf '%s\n' "$issue_url"

Length of output: 2002


@jamesbhobbs Created and assigned #446: Preserve friendly schedule cadence for API-created notebook schedules.

You are interacting with an AI system.

@jamesbhobbs
jamesbhobbs merged commit f9f89d5 into main Aug 5, 2026
21 checks passed
@jamesbhobbs
jamesbhobbs deleted the feat/cloud-scheduling branch August 5, 2026 17:29
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.

2 participants