KeeperHub / KeeperHub/keeperhub

feat: workflow preflight simulates each write node against the state the earlier nodes produced

Open
#2,519 1 comment 0 reactions 0 assignees View on GitHub
accepted confirmed enhancement
Dominant language
TypeScript
Stars
24
Forks
93
Avg merge
1d 8h
Merged PRs (30d)
266

Description

### Before filing

- [x] I searched open and closed issues for this proposal.
- [x] I checked the docs and the current behaviour on `staging`.
- [x] This is one change, not several. (Several means several issues.)

### Reason: what you cannot do today

`POST /api/workflows/{id}/simulate` runs `runWorkflowSimulation` (`lib/workflow/run-simulation.ts`), which
calls `simulateContractCall` once per write node, each against latest chain state. A workflow whose second
write depends on its first - approve then deposit, the shape of nearly every DeFi workflow - warns on the
second every time, because the first has not happened yet.

Reproduced on app.keeperhub.com on 15 Sep 2026 with a two-node workflow, literal arguments, no templates:
`approve(vault, 5000000)` on Base Sepolia USDC, then `deposit(5000000, wallet)` on an ERC-4626 vault
(workflow `a1wee2uy9lqdz0zp4cpry`, deleted after the run). The preflight answered:

```json
{ "simulatedNodeCount": 1,
"warnings": [ { "code": "SIMULATION_WOULD_REVERT", "nodeId": "deposit",
"message": "Deposit 5 USDC may revert: Error(ERC20: transfer amount exceeds allowance). This may depend on an earlier step in this workflow." } ] }
```

The warning is wrong, and the platform knows it might be: the sentence "This may depend on an earlier step in
this workflow" is added by `simulationPreflightMessage` whenever a reachable write precedes the node
(`run-simulation.ts:310-322`). The comment above it says why: "Simulation reads current state, not the state
the workflow will have produced by the time this node runs, so the claim is softened the same way a revert
is." The limitation is documented in the code and hedged in the message. Two things follow from it:

1. Every approve-then-spend workflow gets a warning the author has to learn to ignore, which is how real
warnings get ignored too. 12 of the 43 seeded workflows are that shape (#2367 counted them).
2. "Earlier" is array order, not execution order. The loop at `run-simulation.ts:721` iterates `nodes` as
stored and uses `reachableNodeIds` only as a filter, so `hasEarlierReachableWrite` is true or false by
where a node sits in the JSON, not by what runs before it.

The mechanism that fixes the direct-execution version of this is already on `staging`: #2452 (merged) adds
`simulateCallSequence` in `lib/execute/simulate-sequence.ts` (`eth_simulateV1`, with `debug_traceCall` state
diffs replayed as `eth_call` overrides where a node lacks it), which simulates N calls each against the state
the previous one produced. suisuss scoped the workflow engine out of #2427 so it could ship; this is that
follow-up, and it adds no new mechanism.

### Reason: what the workaround costs

The warning is advisory, so nothing is blocked; the cost is that the preflight cannot be trusted on the
workflow shape it is most needed for. An agent building a workflow through MCP sees "may revert" on a
correct deposit and either reworks a correct workflow or learns to ignore the check. I hit both with a
generated compounder (approve then `morpho/vault-deposit`) in the Almanak integration, and the reason the
keeper there runs with template-bound amounts is partly that the preflight could not tell me anything useful
about literal ones.

### Scope: what this touches, and what it does not

In: `runWorkflowSimulation` groups reachable, enabled, template-free `web3/write-contract` nodes that sit on
one linear path and one chain into a sequence, simulates the sequence with `simulateCallSequence`, and
attributes each result back to its node. A node whose earlier steps were applied loses the "may depend on an
earlier step" hedge, because it no longer does. Nodes the grouping cannot place - after a fork, on a different
chain, following a template-bound node - keep today's per-node simulation and today's hedge, so nothing gets
less informative.

Out: protocol nodes (not simulated today; method resolution for them is #2366's seam), template-bound nodes
(the roadmap's pin schemas), `web3/transfer-funds` (native value, no calldata; stays per-node),
`check-and-execute`, the route, the response shape. Warnings stay advisory: `valid` is not computed here.

One change: a workflow with no chainable run is simulated exactly as today.

### Plan: what you propose

- `lib/workflow/run-simulation.ts`: order reachable nodes by following edges from the trigger rather than
by array index. Collect maximal runs of consecutive chainable nodes (same `network`, single in-edge, single
out-edge, supported type, no template variables). Each run becomes one `simulateCallSequence` call; the
per-call `SimulateResult` maps onto the existing `simulateNode` outcome shape so `makeIssue` and the
counters are untouched. Runs of length one and unplaceable nodes take the existing path.
- `simulationPreflightMessage`: the hedge is added only when the node was not simulated with its earlier
steps applied. Chained nodes get the plain sentence.
- Deadline and node cap unchanged; a run is one round trip where it was N, so this is cheaper, not dearer.
- Tests in `tests/unit/workflow-run-simulation.test.ts`: approve-then-deposit produces no warning when the
sequence answers clean and a plain (unhedged) warning when it answers a revert; a fork ends a run and the
branch nodes simulate per-node with the hedge; a chain change ends a run; a template-bound node ends a run;
a sequence answering `unavailable` falls back to per-node for that run rather than warning on nothing;
execution order follows edges when array order differs; the seeded-workflow baseline
(`validate-workflow-seed-workflows.test.ts`) unchanged.
- Docs: `docs/agent/mcp-test-workflow.md` roadmap line replaced with what is supported; the simulate route's
section in `docs/api/direct-execution.md` gains a paragraph; `specs/api-coverage.json` regenerated.
- Builds on `simulateCallSequence` from #2452, already on `staging`, so the PR sits directly on `staging`. It is
written and passing (33 tests in `workflow-run-simulation.test.ts`, lint, type-check, `check:api-docs`); I will
open it as soon as this is accepted.

### Plan: alternatives you considered

- Keep the hedge and document it: what exists today; the warning stays untrustworthy on the common shape.
- Simulate the whole workflow through the engine with pinned inputs (the roadmap item): the right long-term
answer and a much larger change; this issue is the part of it that needs no pin schema, because it only
covers nodes whose arguments are already literal.
- Reconstruct state per node from the previous node's trace, without `eth_simulateV1`: the fallback #2452
already implements; using the sequence simulator gets both mechanisms for free.

### Scope: compatibility

- [ ] Changes an existing response shape, status code, CLI flag, or default.
- [ ] Adds, removes, or upgrades a dependency.
- [ ] Changes database schema or requires a migration.
- [ ] Touches authentication, permissions, validation, or spend limits.
- [ ] Changes pricing, plan limits, or anything a user is charged.

Contributor guide

Open the contributing guide

Research direction

Start in lib/workflow/run-simulation.ts at runWorkflowSimulation and its node-ordering and preflight-message logic, then read lib/execute/simulate-sequence.ts and the existing tests. Run tests/unit/workflow-run-simulation.test.ts and validate-workflow-seed-workflows.test.ts to understand current outcomes. Done means eligible linear literal write nodes are simulated in execution order, unsupported groupings retain per-node behavior, the documented tests pass, and the listed documentation files and API coverage are updated.

Written by the indexing model from the issue text.

Assessment

Tech stack
blockchain, typescript
Domain
backend-api-design, blockchain, documentation, testing
Issue type
Feature
Difficulty
4/5
Estimated time
3-5 days
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
72/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.