Skip to content

allow inline execution#219

Merged
ianmacartney merged 16 commits intomainfrom
ian/inline-query-mutation
Mar 21, 2026
Merged

allow inline execution#219
ianmacartney merged 16 commits intomainfrom
ian/inline-query-mutation

Conversation

@ianmacartney
Copy link
Copy Markdown
Member

@ianmacartney ianmacartney commented Mar 10, 2026

TL;DR

Added inline execution support for workflow queries and mutations, allowing them to run within the same transaction as the workflow instead of being dispatched through the work pool.

What changed?

  • Added shareTransaction option to workflow definitions that enables inline execution by default for queries and mutations
    • decided to hold off on the workflow-level default for now, as it's not clear what would be shared with what.
  • Added per-call inline option to override the default behavior for individual function calls
  • Implemented inline execution logic in startSteps that runs queries and mutations directly within the workflow transaction
  • Added comprehensive test suite covering sequential queries, parallel queries, Promise.race, mutations, mixed inline/action scenarios, and dependent queries
  • Updated type definitions to support the new inline functionality and made workId optional for inline steps

How to test?

Run the functions in example/convex/inlineTest.ts (see inlineTest.test.ts for example on how to test it - but unfortunately the vitest tests are skipped until we can test workflows without blowing up globals) which covers:

  • Sequential inline queries completing in one poll
  • Parallel inline queries resolving in push order
  • Promise.race behavior with inline queries
  • Inline mutations executing within the same transaction
  • Per-call inline overrides
  • Mixed inline and action scenarios
  • Dependent inline queries where the second uses the first's result

Why make this change?

This change eliminates the round-trip overhead of dispatching queries and mutations through the work pool when they can safely execute within the workflow's transaction. This improves performance for workflows that primarily use queries and mutations, while maintaining the existing behavior for actions and providing flexibility through per-call overrides.

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 10, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds inline execution for queries and mutations via { inline: true } on step.runQuery/step.runMutation, propagates inline through RunOptions/StepRequest, updates journal/workflow to handle pre-completed inline steps and optional workId, and revises docs/examples from ctxstep; adds example workflows and tests.

Changes

Cohort / File(s) Summary
Docs & Changelog
README.md, CHANGELOG.md
Replace ctx framing with step; document deterministic replay, retry semantics, { inline: true } behavior, and add 0.3.7-alpha.0 changelog entry.
Examples & Tests
example/convex/inlineTest.ts, example/convex/inlineTest.test.ts, example/convex/nestedWorkflow.ts, example/convex/passingSignals.ts, example/convex/userConfirmation.ts
Add inline execution example workflows and a skipped Vitest suite; update example handlers to use step and step.* calls.
Client API & Types
src/client/step.ts, src/client/workflowContext.ts, src/types.ts
Introduce inline: boolean on StepRequest and a discriminated RunOptions union (inline vs scheduler); default inline=false; propagate inline flag; make function-step workId optional.
Runtime / Journal / Workflow
src/component/journal.ts, src/component/workflow.ts
Handle steps with pre-populated runResult (emit stepCompleted, skip enqueue), remove strict inProgress assertion in startSteps, and avoid non-null workId assertions when exposing steps.
Package / Scripts
package.json
Bump package version and convex dev dependency; adjust predev script to always run convex init and conditionally build if dist missing.

Sequence Diagram(s)

sequenceDiagram
  participant WF as WorkflowHandler
  participant Step as StepCoordinator
  participant Func as FunctionHandler
  participant Pool as Workpool

  WF->>Step: step.runQuery(..., inline: true)
  alt inline path
    Step->>Func: invoke query/mutation handler (inline)
    Func-->>Step: return result / throw error
    Step-->>WF: provide runResult (completed)
  else workpool path
    WF->>Pool: enqueue step (query/mutation/action)
    Pool->>Func: execute handler asynchronously
    Func-->>Pool: result / error
    Pool-->>WF: deliver completion notification
  end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • sethconvex

Poem

🐇 I hopped into the handler, nimble and bright,
Inline queries whispered and skipped the night,
Results came home early, no workpool to comb,
The workflow danced softly — the rabbit found a home.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title "allow inline execution" accurately describes the main feature addition (inline execution support for queries/mutations in workflows), aligning with the PR's primary objective.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ian/inline-query-mutation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link
Copy Markdown
Member Author

ianmacartney commented Mar 10, 2026

This stack of pull requests is managed by Graphite. Learn more about stacking.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Mar 10, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@convex-dev/workflow@219

commit: e31ad10

@ianmacartney ianmacartney changed the title allow inline allow inline e Mar 10, 2026
@ianmacartney ianmacartney changed the title allow inline e allow inline execution Mar 10, 2026
@ianmacartney ianmacartney requested a review from reeceyang March 10, 2026 20:47
@ianmacartney ianmacartney force-pushed the ian/inline-query-mutation branch from 57b0d94 to ffa4bec Compare March 19, 2026 00:36
Copy link
Copy Markdown
Member Author

@BugBot run

@cursor
Copy link
Copy Markdown

cursor bot commented Mar 19, 2026

Skipping Bugbot: Bugbot is disabled for this repository. Visit the Bugbot dashboard to update your settings.

@ianmacartney ianmacartney marked this pull request as ready for review March 19, 2026 00:53
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (4)
example/convex/inlineTest.ts (2)

59-65: Misleading function name: slowAction is not actually slow.

The function is named slowAction and the comment at line 175 says "action goes through workpool", but the implementation returns immediately without any delay. This could confuse future maintainers trying to understand the test behavior.

Consider either:

  • Adding an actual delay (e.g., await new Promise(r => setTimeout(r, 100)))
  • Renaming to something more accurate like testAction or simpleAction
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/convex/inlineTest.ts` around lines 59 - 65, The function slowAction
defined via internalAction (handler async (_ctx, { label }) => ...) is
misleading because it returns immediately; either add an explicit small delay
inside the handler (e.g., await a short timeout before returning) to match the
"slow" name or rename the exported symbol slowAction to a truthful name like
testAction or simpleAction and update any references; locate the export
slowAction and modify the handler to include the delay or perform a safe rename
across the codebase where slowAction is used.

49-53: Inserting empty string as workflowId bypasses type safety and may cause issues.

The workflowId: "" as any coercion inserts an invalid workflow ID into the flows table. While this works for test purposes, it:

  1. Bypasses TypeScript's type safety with as any
  2. Could cause issues if code elsewhere queries by workflowId index expecting valid IDs
  3. May cause confusion when debugging test data

Consider passing a valid workflow ID through the args if one is available, or using a dedicated test table without the workflowId requirement.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/convex/inlineTest.ts` around lines 49 - 53, The insert into "flows"
uses a type-bypassing workaround (workflowId: "" as any) which should be
replaced with a real test value; modify the code around ctx.db.insert in
inlineTest.ts so workflowId is provided from test args or generated
deterministically (e.g., args.workflowId or a local testId/UUID) and pass that
value into ctx.db.insert("flows", { in: key, workflowId: <validId>, out: newVal
}); alternatively switch to a dedicated test table that doesn't require
workflowId if no valid id is available.
example/convex/inlineTest.test.ts (2)

10-16: Duplicate vi.useFakeTimers() calls.

vi.useFakeTimers() is called at module level (line 10) and again in beforeEach (line 15). The module-level call is redundant since beforeEach already sets up fake timers before each test, and afterEach restores real timers.

Consider removing the module-level call to avoid confusion:

Suggested fix
-vi.useFakeTimers();
-
 // TODO: When we have tests running without messing with globals, enable these
 describe.skip("inline queries and mutations", () => {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/convex/inlineTest.test.ts` around lines 10 - 16, Remove the redundant
module-level call to vi.useFakeTimers() and rely on the existing beforeEach
setup inside the describe.skip("inline queries and mutations") block;
specifically delete the top-level vi.useFakeTimers() so the test suite only
calls vi.useFakeTimers() in beforeEach (with afterEach restoring real timers) to
avoid global/test setup confusion.

6-6: Remove empty import statement.

The import import {} from "@convex-dev/workflow"; imports nothing and appears to be dead code.

Suggested fix
-import {} from "@convex-dev/workflow";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@example/convex/inlineTest.test.ts` at line 6, Remove the dead empty import
statement `import {} from "@convex-dev/workflow";` in the test file; locate the
import line in inlineTest.test.ts (the `import {}` declaration) and delete it so
there are no unused/empty imports left in the module.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Around line 332-334: Fix the grammar in the README sentence referencing
step.runQuery() and step.runMutation(): change "independently transaction" to
"independent transaction" so the sentence reads that calls are dispatched
through the workpool which run the function in an independent transaction; keep
the mention of the inline opt-in ({ inline: true }) unchanged.

In `@src/component/journal.ts`:
- Around line 157-165: The inline-completed branch currently logs stepCompleted
before the unconditional started event later, causing reversed ordering; fix by
emitting the started event before completed when step.runResult is present (call
console.event("stepStarted", { workflowId: entry.workflowId, workflowName:
workflow.name, stepName: step.name, stepNumber }) immediately before the
existing console.event("stepCompleted", ...)), and also guard the later
unconditional console.event("stepStarted", ...) so it only runs when
step.runResult is falsy (e.g., wrap it in if (!step.runResult) {...}) to avoid
duplicate events.

---

Nitpick comments:
In `@example/convex/inlineTest.test.ts`:
- Around line 10-16: Remove the redundant module-level call to
vi.useFakeTimers() and rely on the existing beforeEach setup inside the
describe.skip("inline queries and mutations") block; specifically delete the
top-level vi.useFakeTimers() so the test suite only calls vi.useFakeTimers() in
beforeEach (with afterEach restoring real timers) to avoid global/test setup
confusion.
- Line 6: Remove the dead empty import statement `import {} from
"@convex-dev/workflow";` in the test file; locate the import line in
inlineTest.test.ts (the `import {}` declaration) and delete it so there are no
unused/empty imports left in the module.

In `@example/convex/inlineTest.ts`:
- Around line 59-65: The function slowAction defined via internalAction (handler
async (_ctx, { label }) => ...) is misleading because it returns immediately;
either add an explicit small delay inside the handler (e.g., await a short
timeout before returning) to match the "slow" name or rename the exported symbol
slowAction to a truthful name like testAction or simpleAction and update any
references; locate the export slowAction and modify the handler to include the
delay or perform a safe rename across the codebase where slowAction is used.
- Around line 49-53: The insert into "flows" uses a type-bypassing workaround
(workflowId: "" as any) which should be replaced with a real test value; modify
the code around ctx.db.insert in inlineTest.ts so workflowId is provided from
test args or generated deterministically (e.g., args.workflowId or a local
testId/UUID) and pass that value into ctx.db.insert("flows", { in: key,
workflowId: <validId>, out: newVal }); alternatively switch to a dedicated test
table that doesn't require workflowId if no valid id is available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c39ac27d-d6e8-41e0-9ce2-66bd30d7e113

📥 Commits

Reviewing files that changed from the base of the PR and between eef5d0c and a1529e1.

⛔ Files ignored due to path filters (2)
  • example/convex/_generated/api.d.ts is excluded by !**/_generated/**
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (12)
  • README.md
  • example/convex/inlineTest.test.ts
  • example/convex/inlineTest.ts
  • example/convex/nestedWorkflow.ts
  • example/convex/passingSignals.ts
  • example/convex/userConfirmation.ts
  • package.json
  • src/client/step.ts
  • src/client/workflowContext.ts
  • src/component/journal.ts
  • src/component/workflow.ts
  • src/types.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
package.json (1)

20-20: Guard convex init with an initialization check.

Line 20 unconditionally runs convex init before every predev. Convex's documentation recommends checking for initialization state (specifically .env.local with CONVEX_DEPLOYMENT set and the convex/ directory) before running initialization commands. Consider gating convex init similar to how npm run build is guarded—for example, (path-exists .env.local && path-exists convex) || convex init, to avoid redundant initialization on subsequent runs.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@package.json` at line 20, The predev npm script currently runs "convex init"
unconditionally; change it to first check for initialization artifacts and only
call convex init if missing. Modify the "predev" script (the npm script named
predev) to guard the convex init call by checking for .env.local containing
CONVEX_DEPLOYMENT and the convex/ directory (e.g., use path-exists checks
similar to the existing build guard) so that convex init only runs when those
initialization indicators are absent.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@package.json`:
- Line 20: The predev npm script currently runs "convex init" unconditionally;
change it to first check for initialization artifacts and only call convex init
if missing. Modify the "predev" script (the npm script named predev) to guard
the convex init call by checking for .env.local containing CONVEX_DEPLOYMENT and
the convex/ directory (e.g., use path-exists checks similar to the existing
build guard) so that convex init only runs when those initialization indicators
are absent.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: b4d956eb-c5c5-45d1-991b-5d8cf6ef8ce4

📥 Commits

Reviewing files that changed from the base of the PR and between a1529e1 and 1810535.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (2)
  • CHANGELOG.md
  • package.json
✅ Files skipped from review due to trivial changes (1)
  • CHANGELOG.md

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 26: The phrase "to recover failed workflows after third party outages or
fixing code bugs." should be rewritten for correct parallelism and hyphenation;
change it to something like "to recover failed workflows after third-party
outages or to fix code bugs." Apply the same cleanup to the identical/related
occurrences (the similar sentences around the other mentioned spots) so they use
"third-party" and consistent parallel construction ("after third-party outages
or to fix...") and correct punctuation.
- Line 197: The example workflow invocation uses the wrong argument key: replace
the object literal containing exampleArg (e.g., "{ exampleArg: \"James\" }")
with the correct key "name" to match the workflow's args definition (args: {
name: v.string() }) so the call becomes "{ name: \"James\" }"; update any
related examples or labels referencing exampleArg to use name for consistency.
- Around line 64-69: The example calling step.runMutation is invalid because it
places the options object ({ inline: true }) inside the args object; update the
call to pass the mutation identifier (internal.emails.sendWelcomeEmail), then
the args object ({ userId, content }) as the second argument, and the options
object ({ inline: true }) as the third argument to step.runMutation so the
syntax matches other examples and is valid JavaScript/TypeScript.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: c9fbd620-df91-40c5-b3e7-a334cb286ef9

📥 Commits

Reviewing files that changed from the base of the PR and between 1810535 and d0201ea.

📒 Files selected for processing (4)
  • README.md
  • example/convex/inlineTest.test.ts
  • example/convex/inlineTest.ts
  • src/component/journal.ts
✅ Files skipped from review due to trivial changes (1)
  • example/convex/inlineTest.test.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/component/journal.ts
  • example/convex/inlineTest.ts

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

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

⚠️ Outside diff range comments (1)
README.md (1)

436-436: ⚠️ Potential issue | 🟡 Minor

Typo: "immmediately" has three m's.

📝 Proposed fix
-execution immmediately. In-progress calls to `step.runAction()`, however, will
+execution immediately. In-progress calls to `step.runAction()`, however, will
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 436, Fix the typo in the README where the phrase
"execution immmediately." appears (near the mention of `step.runAction()`);
change "immmediately" to "immediately" so the sentence reads "execution
immediately."
♻️ Duplicate comments (1)
README.md (1)

757-757: ⚠️ Potential issue | 🟡 Minor

Duplicate word: "the the".

📝 Proposed fix
-  functions and passing IDs around within the the workflow.
+  functions and passing IDs around within the workflow.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 757, Replace the duplicated word in the README sentence
"functions and passing IDs around within the the workflow." by removing the
extra "the" so it reads "functions and passing IDs around within the workflow."
Locate that exact phrase in the README and update it accordingly.
🧹 Nitpick comments (1)
README.md (1)

754-754: Consider consistent size unit terminology.

The documentation uses both "1 MB" (line 754) and "8MiB" (line 761). For technical precision, consider using MiB consistently, as these are likely binary units (1024-based). However, if "1 MB" reflects the actual limit as defined in the codebase, this may be intentional.

Also applies to: 761-761

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@README.md` at line 754, Replace the inconsistent size unit wording by
standardizing on MiB (or on MB if code enforces decimal MB) across the README:
update the string "1 MB" to "1 MiB" (or change "8MiB" to "8 MB" if you confirm
the code uses decimal megabytes), and ensure any other occurrences of "MB" vs
"MiB" are made consistent (search for the literals "1 MB" and "8MiB" and update
to the chosen unit), optionally adding a short parenthetical note clarifying
that MiB = 1024^2 bytes if you choose MiB for precision.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@README.md`:
- Line 26: Update the README text to hyphenate the compound adjective
"third-party" in the sentence containing "to recover failed workflows after
third party outages or fixing code bugs" and add the missing verb "is" in the
sentence that should read "the overall workflow is guaranteed to run to
completion" so both phrases read correctly.

---

Outside diff comments:
In `@README.md`:
- Line 436: Fix the typo in the README where the phrase "execution
immmediately." appears (near the mention of `step.runAction()`); change
"immmediately" to "immediately" so the sentence reads "execution immediately."

---

Duplicate comments:
In `@README.md`:
- Line 757: Replace the duplicated word in the README sentence "functions and
passing IDs around within the the workflow." by removing the extra "the" so it
reads "functions and passing IDs around within the workflow." Locate that exact
phrase in the README and update it accordingly.

---

Nitpick comments:
In `@README.md`:
- Line 754: Replace the inconsistent size unit wording by standardizing on MiB
(or on MB if code enforces decimal MB) across the README: update the string "1
MB" to "1 MiB" (or change "8MiB" to "8 MB" if you confirm the code uses decimal
megabytes), and ensure any other occurrences of "MB" vs "MiB" are made
consistent (search for the literals "1 MB" and "8MiB" and update to the chosen
unit), optionally adding a short parenthetical note clarifying that MiB = 1024^2
bytes if you choose MiB for precision.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: db334179-e317-4b28-b291-9e11c3243498

📥 Commits

Reviewing files that changed from the base of the PR and between d0201ea and bdc4cf6.

📒 Files selected for processing (1)
  • README.md

): Promise<unknown> {
const { name, retry, ...schedulerOptions } = opts ?? {};
const { name, retry, inline, ...schedulerOptions } = opts ?? {};
if (inline && ("runAt" in schedulerOptions || "runAfter" in schedulerOptions)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

should we also console.warn if inline is true but the function is an action?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I'm going to add a check in step.ts where it will throw if it's not an action, instead of doing the current behavior of ignoring "inline" if it's not a query/mutation

});

// ── Test 6: Mixed inline + action ─────────────
// The query runs inline (shareTransaction), while the action goes through
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Suggested change
// The query runs inline (shareTransaction), while the action goes through
// The query runs inline, while the action goes through

README.md Outdated
Comment on lines 90 to 91
queries, mutations, and actions into long-lived workflows, and the system will
always fully execute a workflow to completion.
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[Re: lines +89 to +91]

nit: this paragraph seems redundant now

See this comment inline on Graphite.

@ianmacartney ianmacartney merged commit f16afd2 into main Mar 21, 2026
2 of 3 checks passed
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