Visitar URL original
v0.9.15: mship improvements, memory improvements, nextjs bump, plane and ramp integrations, outbox hardening by waleedlatif1 · Pull Request #8748 · simstudioai/sim · GitHub
Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
141 commits
Select commit Hold shift + click to select a range
cb05f50
docs(library): update agentic-ai-coding-tools-what-they-are-and-how-t…
icecrasher321 Oct 5, 2026
1056845
feat(library): How do you build an AI bot for Discord without code? (…
icecrasher321 Oct 5, 2026
733f4e9
perf(sandbox): grant the run_code session lease in the reconnect inst…
waleedlatif1 Oct 5, 2026
dc10d4c
fix(chat): label simple effort options with the effort they send (#8619)
waleedlatif1 Oct 5, 2026
807523e
feat(workflows): stop a manual v2 run after a block, and `workflows r…
waleedlatif1 Oct 5, 2026
b2c5b76
fix(executor): drop the full JSON clone of execution state at run com…
waleedlatif1 Oct 5, 2026
c723046
fix(logs): keep a block log's file size from changing after the block…
waleedlatif1 Oct 5, 2026
ea88578
feat(projects): add project identity and lifecycle foundation (#8580)
mzxchandra Oct 5, 2026
a865f47
fix(mcp): restrict MCP server destination changes to admins (#8629)
TheodoreSpeaks Oct 5, 2026
2c8eeaf
fix(code-placeholders): reject shell arithmetic placeholders (#8628)
icecrasher321 Oct 5, 2026
6b3cd82
fix(credentials): keep the field-less GitHub App installation out of …
TheodoreSpeaks Oct 5, 2026
844d181
fix(executor): fail a stop-after run whose routing skips the stop blo…
waleedlatif1 Oct 5, 2026
21c8a07
chore(terms): update terms of service (#8636)
waleedlatif1 Oct 5, 2026
7050250
fix(projects): restore workspace deletion and tighten Project lifecyc…
waleedlatif1 Oct 5, 2026
d32d4c9
feat(mothership): keep each chat's reasoning effort, default to mediu…
waleedlatif1 Oct 5, 2026
bd06886
feat(library): Best AI Coding Agents for Refactoring and Debugging: 5…
icecrasher321 Oct 5, 2026
6845fd9
feat(library): Best Conversational AI Platforms With CRM and Helpdesk…
icecrasher321 Oct 5, 2026
42c972c
docs(library): update langgraph-alternatives (#8640)
icecrasher321 Oct 5, 2026
8524cb9
docs(library): update dify-alternatives (#8641)
icecrasher321 Oct 5, 2026
2613543
docs(library): update best-ai-agents-for-customer-support-automation …
icecrasher321 Oct 5, 2026
2b007e1
docs(library): update ai-agent-observability (#8645)
icecrasher321 Oct 6, 2026
73bb265
docs(library): update aeo-vs-geo-what-answer-engine-and-generative-en…
icecrasher321 Oct 6, 2026
75cc375
docs(library): update mcp-security (#8649)
icecrasher321 Oct 6, 2026
2d043e9
docs(library): update ai-agents-in-procurement (#8648)
icecrasher321 Oct 6, 2026
e8e946c
docs(library): update sim-open-source-zapier-alternative (#8646)
icecrasher321 Oct 6, 2026
d4f26a7
fix(seo): make content pagination indexable (#8651)
waleedlatif1 Oct 6, 2026
46cd710
fix(mothership): refuse desktop claims for unapproved or stopped call…
waleedlatif1 Oct 6, 2026
269f9b8
fix(mothership): roll back a failed effort save by pick, not by value…
waleedlatif1 Oct 6, 2026
20a7350
fix(mcp): treat different URL credentials as a new destination (#8656)
TheodoreSpeaks Oct 6, 2026
3064219
fix(executor): unwrap boxed primitives by internal slot when checking…
waleedlatif1 Oct 6, 2026
777b3e9
fix(projects): revoke banned users' keys first and create imported wo…
waleedlatif1 Oct 6, 2026
648bf45
fix(mothership): default chat effort back to high (#8659)
waleedlatif1 Oct 6, 2026
239526a
improvement(tables): read row counts and versions through an append-o…
TheodoreSpeaks Oct 6, 2026
70e1980
docs(workflows): describe what still runs when a stop-after target is…
waleedlatif1 Oct 6, 2026
ef59524
test(mcp): exercise the real destination check in lifecycle tests (#8…
TheodoreSpeaks Oct 6, 2026
9af89fc
fix(mothership): fail unclaimed desktop calls fast and stop dropping …
waleedlatif1 Oct 6, 2026
c98e3c7
fix(sandbox): judge session lease coverage from the request's own clo…
waleedlatif1 Oct 6, 2026
a766a03
fix(mothership): keep a new chat's effort pick changed while its firs…
waleedlatif1 Oct 6, 2026
ba12945
feat(desktop): device registry, inbox and leased claims for a backgro…
waleedlatif1 Oct 6, 2026
65b247f
fix(mothership): keep local file tools running when the chat view cha…
waleedlatif1 Oct 6, 2026
63ba307
docs(library): update best-multi-agent-frameworks-2026 (#8678)
icecrasher321 Oct 6, 2026
b20ddc1
docs(library): update open-source-ai-agent-platforms (#8679)
icecrasher321 Oct 6, 2026
32a7f8e
fix(copilot): store tool file outputs from workspace chats under the …
waleedlatif1 Oct 6, 2026
7308e45
feat(library): 25 Multi-Step AI Agent Examples Beyond Chatbots (#8677)
icecrasher321 Oct 6, 2026
c9c2c4e
feat(plane): add core integration with self-hosted support (#8660)
waleedlatif1 Oct 6, 2026
d8ba4bf
feat(sso): allow self-hosted DNS verification bypass (#8681)
waleedlatif1 Oct 6, 2026
5156261
feat(desktop): bind turns to a desktop and enforce its deadlines from…
waleedlatif1 Oct 6, 2026
ab25c2a
docs(library): update ai-agent-examples-by-department-and-industry (#…
icecrasher321 Oct 6, 2026
94cba3b
fix(mothership): end desktop tools at sign-out and settle held report…
waleedlatif1 Oct 6, 2026
82591b5
docs(library): update top-ai-assistants-2026 (#8683)
icecrasher321 Oct 6, 2026
7307778
docs(library): update sim-vs-dedicated-chatbot-builders (#8684)
icecrasher321 Oct 6, 2026
5fccf71
docs(library): update best-ai-agents-for-lead-enrichment-2026 (#8687)
icecrasher321 Oct 6, 2026
c8ef18d
fix(desktop): inbox persistence order, desktop-only overdue sweep, an…
waleedlatif1 Oct 6, 2026
1f114dd
fix(mothership): re-read the transcript until the server has saved a …
waleedlatif1 Oct 6, 2026
662bd70
fix(mothership): re-attach at once when the user returns to a recover…
waleedlatif1 Oct 6, 2026
8714a8e
fix(mothership): keep a message the server never admitted instead of …
waleedlatif1 Oct 6, 2026
62287f6
test(desktop): keep the SQL and TypeScript desktop-call classifiers i…
waleedlatif1 Oct 6, 2026
5d86e42
docs(library): update 6-best-ai-observability-tools-for-production-ag…
icecrasher321 Oct 6, 2026
482f146
fix(plane): correct tile styling and capped connector syncs (#8690)
waleedlatif1 Oct 6, 2026
11f8190
fix(ci): stop each HTTP end-to-end app fully and start the next from …
waleedlatif1 Oct 6, 2026
3cc1ec1
fix(plane): validate webhook fields by registration mode (#8698)
waleedlatif1 Oct 6, 2026
d59e923
improvement(trigger): downsize connector sync and document processing…
waleedlatif1 Oct 6, 2026
551e05f
feat(mothership): let the worker choose models when the model picker …
waleedlatif1 Oct 6, 2026
5f1a29e
perf(cli): load a manual run's draft once and keep embedded CLI resul…
waleedlatif1 Oct 6, 2026
f4855cd
fix(mothership): prune composer contexts during render so fast typing…
waleedlatif1 Oct 6, 2026
632ba60
feat(desktop): run a chat's desktop tools in the background executor …
waleedlatif1 Oct 6, 2026
f98df1b
fix(e2e): compile every timed route first in the HTTP suites and stop…
waleedlatif1 Oct 7, 2026
6adf4c6
fix(mothership): close the remaining ways a chat send is lost, resent…
waleedlatif1 Oct 7, 2026
5ee756f
chore(table): remove unused table-update background task (#8708)
waleedlatif1 Oct 7, 2026
8fc4c1a
fix(analytics): protect production Google measurement (#8710)
waleedlatif1 Oct 7, 2026
db30e1a
improvement(triggers): move TRIGGER_TYPES to a dependency-free module…
waleedlatif1 Oct 7, 2026
ca11667
fix(chat): resolve a new chat's history entry to the chat route on Ba…
waleedlatif1 Oct 7, 2026
0d74834
feat(desktop): show background desktop chats and keep the chat view d…
waleedlatif1 Oct 7, 2026
d4d16a0
feat(desktop): keep the machine awake for background chats and yield …
waleedlatif1 Oct 7, 2026
6713fab
feat(desktop): import local files from the background executor (#8672)
waleedlatif1 Oct 7, 2026
da993c5
fix(events): start every SSE response once its subscriptions are live…
waleedlatif1 Oct 7, 2026
351b8b9
fix(mothership): keep a chatless surface's queue across remounts and …
waleedlatif1 Oct 7, 2026
e82689a
fix(ci): stop the detached telemetry flush too, re-signal stragglers …
waleedlatif1 Oct 7, 2026
ccce7f7
fix(webhooks): skip duplicate webhook deliveries already in progress …
waleedlatif1 Oct 7, 2026
ab6b400
chore(trigger): fix queues that never serialized and schedule missing…
waleedlatif1 Oct 7, 2026
b5a6d53
fix(mothership): run same-workflow workflow tools in order instead of…
waleedlatif1 Oct 7, 2026
18ad8b8
improvement(cron): only start outbox and file-search passes when work…
waleedlatif1 Oct 7, 2026
dfe8ecf
fix(desktop): read tmux format output with a separator no tmux versio…
waleedlatif1 Oct 7, 2026
ab588d4
fix(events): authorize the events queued before a stream opens once (…
waleedlatif1 Oct 7, 2026
988b079
fix(mothership): keep a withdrawn first message held at the queue hea…
waleedlatif1 Oct 7, 2026
29038b3
fix(ci): match E2E app tags literally, make them unique per run, and …
waleedlatif1 Oct 7, 2026
38942a7
fix(desktop): run an agent command on tmux that cannot tag its pane, …
waleedlatif1 Oct 7, 2026
2e7082f
fix(mothership): keep any queued send the server may already hold fro…
waleedlatif1 Oct 7, 2026
c5d07cc
fix(settings): prevent false unsaved prompts and preserve drafts (#8704)
waleedlatif1 Oct 7, 2026
48cbd4f
fix(ci): tidy stop-session.sh scans, listings and arguments (#8724)
waleedlatif1 Oct 7, 2026
00cedfa
fix(mothership): claim under the chat lock in the Stop-versus-recover…
waleedlatif1 Oct 7, 2026
edb2e6b
chore(nextjs): upgrade to 16.4.0 (#8729)
waleedlatif1 Oct 7, 2026
ae0a420
fix(mothership): retry a send that failed while the browser stayed on…
waleedlatif1 Oct 7, 2026
334b9f3
fix(desktop): stop the agent's tmux runs a previous process left goin…
waleedlatif1 Oct 7, 2026
566cea6
fix(desktop): frame each tmux format record so a newline in a field c…
waleedlatif1 Oct 7, 2026
1741ff9
test(mothership): stop the online case of the accepted-resend test pa…
waleedlatif1 Oct 7, 2026
055ff9c
improvement(outbox): load handler modules only for the event types a …
waleedlatif1 Oct 7, 2026
c3475fe
fix(mothership): decide once whether a queued send may already be on …
waleedlatif1 Oct 7, 2026
8f617da
refactor(desktop): one forward-only state per recorded tmux run, and …
waleedlatif1 Oct 7, 2026
102019b
test(desktop): stop the agent's terminal commands in the real app, wi…
waleedlatif1 Oct 7, 2026
96014bd
fix(mothership): keep a failed direct send ahead of follow-ups queued…
waleedlatif1 Oct 7, 2026
61ac093
fix(desktop): leave a stopped native file tool's outcome to Stop (#8732)
waleedlatif1 Oct 7, 2026
435d413
test(desktop): pin the journal snapshot before recovery, keep start g…
waleedlatif1 Oct 7, 2026
f83d977
refactor(mothership): one re-queue policy and one send payload, and k…
waleedlatif1 Oct 7, 2026
af5501b
improvement(desktop): Electron E2E for desktop tool lifetimes against…
waleedlatif1 Oct 7, 2026
f0e5700
fix(mothership): send a late queue write to the chat a new-chat queue…
waleedlatif1 Oct 7, 2026
3899fde
refactor(mothership): one hold field and one retry field on a queued …
waleedlatif1 Oct 7, 2026
00b6cee
fix(mothership): a pure resend verdict, and Send-now drops a message …
waleedlatif1 Oct 7, 2026
9e953d1
refactor(mothership): keep a queued send's reused id in one field (#8…
waleedlatif1 Oct 7, 2026
5d3806b
fix(desktop): keep a chat-view import alive while it works, by the le…
waleedlatif1 Oct 7, 2026
8760ca7
refactor(mothership): a saved Stop handoff's id wins on restore (#8747)
waleedlatif1 Oct 7, 2026
b74b9dd
fix(ci): count both sides of a rename in the desktop live gate, keep …
waleedlatif1 Oct 7, 2026
1a4a156
fix(desktop): linear path trim, prompt Stop between keystrokes, clear…
waleedlatif1 Oct 7, 2026
a7aaa2c
fix(outbox): log unloaded handler modules as a failure, not a complet…
waleedlatif1 Oct 7, 2026
9a1aca0
fix(desktop): stop a run the model never got the result of (#8749)
waleedlatif1 Oct 7, 2026
69d9ac6
fix(mothership): close a pre-aborted SSE stream, keep the new-chat ef…
waleedlatif1 Oct 7, 2026
6a92e05
fix(mothership): use a spinner while workflow resources load (#8756)
waleedlatif1 Oct 7, 2026
14e6c0a
improvement(mothership): relabel the Sol pick GPT-6.1 Sol and retire …
waleedlatif1 Oct 7, 2026
b4c38df
feat(providers): add Nebius Token Factory (#8759)
waleedlatif1 Oct 7, 2026
9ab24e9
ci: faster, cheaper, consistently named CI (#8750)
waleedlatif1 Oct 7, 2026
f775b29
fix(settings): protect generated keys and identity transitions (#8761)
waleedlatif1 Oct 7, 2026
bd7085e
fix(sandbox): keep reused E2B workbench leases within the runtime cap…
waleedlatif1 Oct 7, 2026
d8cb659
ci: duration-balanced integration shards, warm desktop-live, CI on ev…
waleedlatif1 Oct 7, 2026
b7fa794
fix(mothership): cancel desktop tools a signed-out turn starts late, …
waleedlatif1 Oct 7, 2026
96090ca
fix(tables): stop the fold sweep from starting a page query past its …
TheodoreSpeaks Oct 7, 2026
7b287b2
fix(docs): prerender complete LLM documentation (#8767)
waleedlatif1 Oct 7, 2026
350b016
improvement(ci): check out same-repo pull requests through a git mirr…
waleedlatif1 Oct 7, 2026
6df74d6
fix(docs): publish full LLM text as a static asset (#8769)
waleedlatif1 Oct 7, 2026
886cfa2
fix(billing): converge Stripe subscription syncs and stop webhook ech…
waleedlatif1 Oct 8, 2026
b7d5532
perf(pii): batch spaCy NER passes in redact_batch (#8772)
TheodoreSpeaks Oct 8, 2026
1f1e25d
improvement(files): add sharing to file resource view (#8775)
waleedlatif1 Oct 8, 2026
c53367c
feat(permission-groups): add agent default (#8771)
waleedlatif1 Oct 8, 2026
37f5cdf
feat(integrations): add Ramp and Microsoft Intune (#8780)
waleedlatif1 Oct 8, 2026
df34c67
feat(models): add current frontier models (#8777)
waleedlatif1 Oct 8, 2026
1f8c281
fix(executor): preserve stop-after across pause and resume (#8776)
waleedlatif1 Oct 8, 2026
59e749e
fix(audit): harden data drains and expand security event coverage (#8…
waleedlatif1 Oct 8, 2026
31a911d
improvement(desktop): ship the background executor without a flag, cl…
waleedlatif1 Oct 8, 2026
684b228
feat(api): expose credential sharing and SSO administration (#8770)
waleedlatif1 Oct 8, 2026
c93fe67
feat(buffer): complete stable API tool coverage (#8779)
waleedlatif1 Oct 8, 2026
b6861b3
fix(desktop): poll desktop activity only for users with a desktop, ke…
waleedlatif1 Oct 8, 2026
e3800f6
fix(code-placeholders): reject arithmetic comparisons and heredoc ope…
waleedlatif1 Oct 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
feat(workflows): stop a manual v2 run after a block, and `workflows r…
…un --stop-after` (#8622)

* feat(workflows): stop a manual v2 run after a block, and `workflows run --stop-after`

A manual v2 run can now name `run.stopAfterBlockId`; the run stops once that
block completes and downstream blocks do not execute. Combined with a block
entry on the same block, it re-runs exactly one block against a prior run's
persisted upstream outputs, server-side:

  sim workflows run W --from-block X --source-run R --stop-after X --select-output X.result

Agents verifying an edit no longer re-run every upstream block (often a slow
LLM or API call) or toggle blocks off to skip them.

- Contract: optional `stopAfterBlockId` on the manual run selection.
- Application: both manual operations refuse a block missing from the saved
  workflow or nested in a loop/parallel (the engine would otherwise run to the
  end or stop after one iteration), before anything runs.
- Execute service: threads the trusted value to the sync and stream paths.
- CLI: `--stop-after <blockId>` implies --manual and rejects --async.
- E2E: test-workflow-stop-after-e2e.ts against a running app; the http-e2e job
  gains a Redis service because hosted billing admits runs through a Redis
  usage reservation.

* fix(workflows): refuse stop targets a run cannot reach, and run the E2E self-hosted

- The manual operations refuse a stop block the run cannot reach from its
  entry (an upstream block would let the run finish everything after the
  entry), and look blocks up as own properties.
- The executor fails a run whose stop block is absent from the workflow it
  executes, instead of running everything; this closes the window between
  validation and the executor's own draft load, for every caller.
- The CLI refuses an empty --stop-after rather than dropping it.
- CI: the stop-after E2E gets its own self-hosted app step; the SCIM suite
  asserts PostgreSQL rate-limit storage, so Redis is not added to that app.
  Fixture cleanup waits for run logs to finalize before deleting.

* fix(workflows): refuse a disabled stop block, or one reached only through one

The executor omits disabled blocks from its graph, so a disabled stop target,
or one whose only path runs through a disabled block, is never reached and
the run would finish everything after the entry.

* fix(workflows): the executor refuses a disabled stop block too

The serialized workflow keeps disabled blocks, but the DAG skips them, so a
disabled stop target would never be reached.

---------

Co-authored-by: Waleed Latif <waleed@sim.ai>
  • Loading branch information
waleedlatif1 and Waleed Latif authored Oct 5, 2026
commit 807523eddac25ee49481faeadfe59fb2b75fc358
45 changes: 45 additions & 0 deletions .github/workflows/test-build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,51 @@ jobs:
VERSION_COMPARE_E2E_REPORT_PATH="$report_dir/version-compare-http-report.json" \
bun run test:workflow-version-compare:e2e

# A self-hosted app: hosted billing admits a run only through a Redis usage
# reservation, and the SCIM suite above asserts PostgreSQL rate-limit storage,
# so workflow execution gets its own app rather than adding Redis to that one.
- name: Verify single-block workflow runs over real HTTP
working-directory: apps/sim
env:
NEXT_PUBLIC_APP_URL: http://127.0.0.1:3018
BETTER_AUTH_URL: http://127.0.0.1:3018
NEXT_PUBLIC_FORCE_HOSTED: 'false'
INTERNAL_API_SECRET: stop-after-http-ci-local-secret-at-least-32-characters
DB_TX_TRIPWIRE: throw
DISABLE_TELEMETRY: 'true'
NEXT_TELEMETRY_DISABLED: '1'
NEXT_PUBLIC_CHAT_DISABLED: 'true'
READY_TIMEOUT_SECONDS: 300
run: |
report_dir="$RUNNER_TEMP/e2e"
server_log="$report_dir/stop-after-next.log"
mkdir -p "$report_dir"
node ../../node_modules/next/dist/bin/next dev --hostname 127.0.0.1 --port 3018 > "$server_log" 2>&1 &
server_pid=$!
finish() {
kill "$server_pid" 2>/dev/null || true
wait "$server_pid" 2>/dev/null || true
awk '/^ (GET|POST|PUT|PATCH|DELETE|HEAD) \/api\// { print }' "$server_log" > "$report_dir/stop-after-http-status.log"
}
trap finish EXIT
fail_startup() {
echo "::error::$1"
tail -n 200 "$server_log"
exit 1
}
started=$SECONDS
until curl --fail --silent --max-time 10 http://127.0.0.1:3018/api/health > /dev/null; do
kill -0 "$server_pid" 2>/dev/null || fail_startup 'Local workflow app exited during startup.'
[ $((SECONDS - started)) -lt "$READY_TIMEOUT_SECONDS" ] ||
fail_startup "Local workflow app did not become ready within $READY_TIMEOUT_SECONDS seconds."
sleep 2
done
echo "Local workflow app ready after $((SECONDS - started))s"
STOP_AFTER_E2E_BASE_URL="$NEXT_PUBLIC_APP_URL" \
STOP_AFTER_E2E_DATABASE_URL="$DATABASE_URL" \
STOP_AFTER_E2E_REPORT_PATH="$report_dir/stop-after-http-report.json" \
bun run test:workflow-stop-after:e2e

- name: Upload end-to-end reports and server logs
if: failure()
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/cli/reference.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6704,6 +6704,7 @@ sim workflows run <workflowId> [options]
| `--mock-payload` | No | Use the selected trigger's server-derived mock payload; runs the current saved workflow state (implies --manual). |
| `--from-block <blockId>` | No | Run manually from this saved workflow block. |
| `--source-run <runId>` | No | Prior run whose persisted state supplies upstream outputs (requires --from-block). |
| `--stop-after <blockId>` | No | Stop the run after this saved block; with --from-block on the same block, re-runs only that block (implies --manual). |
| `--follow` | No | Stream the run as it happens; progress on stderr, result on stdout. The stream reports only success and output, so the result omits the run id and timings a non-streaming run returns. |
| `--include-thinking` | No | Show model reasoning while following (requires --follow). |
| `--include-tool-calls` | No | Show tool calls while following (requires --follow). |
Expand Down
1 change: 1 addition & 0 deletions apps/docs/content/docs/cli/workflows.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -642,6 +642,7 @@ sim workflows run <workflowId> [options]
| `--mock-payload` | No | Use the selected trigger's server-derived mock payload; runs the current saved workflow state (implies --manual). |
| `--from-block <blockId>` | No | Run manually from this saved workflow block. |
| `--source-run <runId>` | No | Prior run whose persisted state supplies upstream outputs (requires --from-block). |
| `--stop-after <blockId>` | No | Stop the run after this saved block; with --from-block on the same block, re-runs only that block (implies --manual). |
| `--follow` | No | Stream the run as it happens; progress on stderr, result on stdout. The stream reports only success and output, so the result omits the run id and timings a non-streaming run returns. |
| `--include-thinking` | No | Show model reasoning while following (requires --follow). |
| `--include-tool-calls` | No | Show tool calls while following (requires --follow). |
Expand Down
16 changes: 16 additions & 0 deletions apps/docs/openapi-v2-workflows.json
Original file line number Diff line number Diff line change
Expand Up @@ -12615,6 +12615,11 @@
"additionalProperties": false
}
]
},
"stopAfterBlockId": {
"description": "Saved workflow block after which the run stops; downstream blocks do not execute. Must not be inside a loop or parallel. With a block entry naming the same block, re-runs only that block against the source run.",
"type": "string",
"minLength": 1
}
},
"required": ["source"],
Expand Down Expand Up @@ -12703,6 +12708,17 @@
"sourceRunId": "run_123"
}
}
},
{
"run": {
"source": "manual",
"entry": {
"type": "block",
"blockId": "block_123",
"sourceRunId": "run_123"
},
"stopAfterBlockId": "block_123"
}
}
]
},
Expand Down
2 changes: 2 additions & 0 deletions apps/sim/app/api/v2/workflows/[workflowId]/execute/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,7 @@ export const POST = withRouteHandler(
mode: body.stream ? 'stream' : resultStream ? 'sync-result-stream' : 'sync',
blockId: manualRun.entry.blockId,
sourceRunId: manualRun.entry.sourceRunId,
stopAfterBlockId: manualRun.stopAfterBlockId,
},
request: req,
})
Expand All @@ -460,6 +461,7 @@ export const POST = withRouteHandler(
mode: body.stream ? 'stream' : resultStream ? 'sync-result-stream' : 'sync',
triggerBlockId: manualRun.entry?.blockId,
useMockPayload: manualRun.entry?.useMockPayload === true,
stopAfterBlockId: manualRun.stopAfterBlockId,
},
request: req,
})
Expand Down
14 changes: 14 additions & 0 deletions apps/sim/lib/api/contracts/v2/workflows.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1297,6 +1297,13 @@ export const v2WorkflowRunSelectionSchema = z.discriminatedUnion('source', [
.describe(
'Manual entry mode. Omit to enter through the workflow trigger; a block entry requires an exact source run.'
),
stopAfterBlockId: z
.string()
.min(1, 'run.stopAfterBlockId cannot be empty')
.optional()
.describe(
'Saved workflow block after which the run stops; downstream blocks do not execute. Must not be inside a loop or parallel. With a block entry naming the same block, re-runs only that block against the source run.'
),
})
.strict(),
])
Expand Down Expand Up @@ -1412,6 +1419,13 @@ export const v2ExecuteWorkflowBodySchema = z
entry: { type: 'block', blockId: 'block_123', sourceRunId: 'run_123' },
},
},
{
run: {
source: 'manual',
entry: { type: 'block', blockId: 'block_123', sourceRunId: 'run_123' },
stopAfterBlockId: 'block_123',
},
},
],
})
export type V2ExecuteWorkflowBody = z.input<typeof v2ExecuteWorkflowBodySchema>
Expand Down
118 changes: 118 additions & 0 deletions apps/sim/lib/workflows/application/execute-manual-workflow.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,124 @@ describe('manual workflow execution application operations', () => {
expect(mocks.loadSourceState).not.toHaveBeenCalled()
})

it('rejects a stop block missing from the saved workflow before anything runs', async () => {
await expect(
executeManualWorkflowOperation.execute({
principal,
input: { ...baseInput, useMockPayload: false, stopAfterBlockId: 'missing' },
})
).rejects.toMatchObject({ code: 'validation', message: expect.stringContaining('not a block') })
await expect(
executeManualWorkflowFromBlockOperation.execute({
principal,
input: {
...baseInput,
blockId: 'agent-1',
sourceRunId: 'source-run-1',
stopAfterBlockId: 'missing',
},
})
).rejects.toMatchObject({ code: 'validation', message: expect.stringContaining('not a block') })
expect(mocks.loadSourceState).not.toHaveBeenCalled()
expect(mocks.executeService).not.toHaveBeenCalled()
})

it('rejects a stop block nested in a loop or parallel, which the engine cannot stop on', async () => {
mockLoadManualState.mockResolvedValue({
blocks: {
'trigger-1': {},
'loop-1': { type: 'loop' },
'agent-1': { data: { parentId: 'loop-1' } },
},
edges: [],
})

await expect(
executeManualWorkflowOperation.execute({
principal,
input: { ...baseInput, useMockPayload: false, stopAfterBlockId: 'agent-1' },
})
).rejects.toMatchObject({ code: 'validation', message: expect.stringContaining('inside loop') })
expect(mocks.executeService).not.toHaveBeenCalled()
})

it('rejects a stop block the run cannot reach from its entry, which would run everything', async () => {
mockLoadManualState.mockResolvedValue({
blocks: { 'trigger-1': {}, 'agent-1': {}, 'agent-2': {}, unconnected: {} },
edges: [
{ source: 'trigger-1', target: 'agent-1' },
{ source: 'agent-1', target: 'agent-2' },
],
})

await expect(
executeManualWorkflowFromBlockOperation.execute({
principal,
input: {
...baseInput,
blockId: 'agent-2',
sourceRunId: 'source-run-1',
stopAfterBlockId: 'agent-1',
},
})
).rejects.toMatchObject({
code: 'validation',
message: expect.stringContaining('not reachable'),
})
await expect(
executeManualWorkflowOperation.execute({
principal,
input: { ...baseInput, useMockPayload: false, stopAfterBlockId: 'unconnected' },
})
).rejects.toMatchObject({
code: 'validation',
message: expect.stringContaining('not reachable'),
})
expect(mocks.loadSourceState).not.toHaveBeenCalled()
expect(mocks.executeService).not.toHaveBeenCalled()
})

it('rejects a disabled stop block, or one reached only through a disabled block', async () => {
mockLoadManualState.mockResolvedValue({
blocks: {
'trigger-1': {},
'agent-1': { enabled: false },
'agent-2': {},
},
edges: [
{ source: 'trigger-1', target: 'agent-1' },
{ source: 'agent-1', target: 'agent-2' },
],
})

await expect(
executeManualWorkflowOperation.execute({
principal,
input: { ...baseInput, useMockPayload: false, stopAfterBlockId: 'agent-1' },
})
).rejects.toMatchObject({ code: 'validation', message: expect.stringContaining('is disabled') })
await expect(
executeManualWorkflowOperation.execute({
principal,
input: { ...baseInput, useMockPayload: false, stopAfterBlockId: 'agent-2' },
})
).rejects.toMatchObject({
code: 'validation',
message: expect.stringContaining('not reachable'),
})
expect(mocks.executeService).not.toHaveBeenCalled()
})

it('rejects a stop block named by an inherited object key', async () => {
await expect(
executeManualWorkflowOperation.execute({
principal,
input: { ...baseInput, useMockPayload: false, stopAfterBlockId: 'toString' },
})
).rejects.toMatchObject({ code: 'validation', message: expect.stringContaining('not a block') })
expect(mocks.executeService).not.toHaveBeenCalled()
})

it('rejects a source run without persisted state for this workflow', async () => {
mocks.loadSourceState.mockResolvedValueOnce(null)

Expand Down
67 changes: 67 additions & 0 deletions apps/sim/lib/workflows/application/execute-manual-workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ interface ManualExecutionInput
extends Omit<ExecuteWorkflowInput, 'input' | 'mode' | 'requestedTimeoutSeconds'> {
input?: unknown
mode: 'sync' | 'stream' | 'sync-result-stream'
stopAfterBlockId?: string
}

export interface ExecuteManualWorkflowInput extends ManualExecutionInput {
Expand Down Expand Up @@ -47,6 +48,69 @@ async function loadManualState(workflowId: string) {
return state
}

type ManualWorkflowState = Awaited<ReturnType<typeof loadManualState>>

/** Blocks a run entering at `entryBlockId` can reach; the executor skips disabled blocks. */
function reachableFrom(state: ManualWorkflowState, entryBlockId: string): Set<string> {
const targetsBySource = new Map<string, string[]>()
for (const edge of state.edges) {
const targets = targetsBySource.get(edge.source) ?? []
targets.push(edge.target)
targetsBySource.set(edge.source, targets)
}
const reached = new Set([entryBlockId])
const queue = [entryBlockId]
for (let next = queue.pop(); next !== undefined; next = queue.pop()) {
for (const target of targetsBySource.get(next) ?? []) {
if (reached.has(target) || state.blocks[target]?.enabled === false) continue
reached.add(target)
queue.push(target)
}
}
return reached
}

/**
* The engine stops only when it completes a node whose id equals the target, so
* a target the run cannot reach would silently run everything after the entry:
* an unknown or disabled block, a block upstream of the entry or only behind a
* disabled one, or a block inside a loop or parallel (which would stop after its
* first iteration or never). All are refused, matching the editor, which offers
* "Run until block" only outside subflows.
*/
function assertStopAfterBlock(
state: ManualWorkflowState,
blockId: string | undefined,
entryBlockId: string
): void {
if (blockId === undefined) return
const block = Object.hasOwn(state.blocks, blockId) ? state.blocks[blockId] : undefined
if (!block) {
throw new OrchestrationError(
'validation',
`run.stopAfterBlockId "${blockId}" is not a block in the current saved workflow.`
)
}
if (block.enabled === false) {
throw new OrchestrationError(
'validation',
`run.stopAfterBlockId "${blockId}" is disabled, so the run never executes it.`
)
}
if (block.data?.parentId) {
throw new OrchestrationError(
'validation',
`run.stopAfterBlockId "${blockId}" is inside loop or parallel "${block.data.parentId}"; stop after that container instead.`
)
}
if (!reachableFrom(state, entryBlockId).has(blockId)) {
throw new OrchestrationError(
'validation',
`run.stopAfterBlockId "${blockId}" is not reachable from entry block "${entryBlockId}"; stop after the entry block or one downstream of it.`
)
}
}

function listTriggers(options: ReturnType<typeof resolveTriggerRunOptions>): string {
return options.map((option) => `${option.triggerBlockId} (${option.blockName})`).join(', ')
}
Expand All @@ -68,6 +132,7 @@ function executionServiceInput(params: {
includeFileBase64: params.input.includeFileBase64,
base64MaxBytes: params.input.base64MaxBytes,
selectedOutputs: params.input.selectedOutputs,
stopAfterBlockId: params.input.stopAfterBlockId,
rateLimitCounter: 'sync' as const,
abortSignal: params.input.abortSignal,
mode: params.input.mode,
Expand Down Expand Up @@ -115,6 +180,7 @@ export const executeManualWorkflowOperation = defineAuthorizedWorkflowUseCase({
)
}

assertStopAfterBlock(state, input.stopAfterBlockId, selected.triggerBlockId)
const executionInput = input.useMockPayload ? selected.mockPayload : input.input
const validation = validateTriggerInput(selected, executionInput)
if (!validation.ok) {
Expand All @@ -141,6 +207,7 @@ export const executeManualWorkflowFromBlockOperation = defineAuthorizedWorkflowU
`run.entry.blockId "${input.blockId}" is not a block in the current saved workflow.`
)
}
assertStopAfterBlock(state, input.stopAfterBlockId, input.blockId)

const sourceSnapshot = await getExecutionStateForWorkflow(input.sourceRunId, context.workflowId)
if (!sourceSnapshot) {
Expand Down
Loading
Loading