Skip to content
ƒ FlowscriptDocs
ftw.trade docs ↗
Documentation
Build with it / Flowscript

Dependent actions and recovery

Compose primitive actions on confirmed settlement, inspect captured inputs, handle each failed leg and retain uncertain submissions through recovery.

Put a dependent action inside its predecessor's .onFill, and give each forward and recovery action its own failure handlers. A group of actions is not an atomic transaction. A later failure does not undo an earlier fill.

This guide describes an external application embedding Flowscript with requestRun. The standalone CLI emits unsent intents and does not execute these requests. ftw.trade's protocol followers have their own execution and reconciliation paths; compiling this syntax does not install a correlated settlement executor in that product.

ftw.trade's native protocol and spot paper books execute these continuations at the modeled fill or refusal. Its multi-book paper runner supports the native onchain protocol actions and market spot actions (paper.buy, buyPct, sell, sellPct). Other book and service actions with outcome handlers are refused at strategy load because that runner does not report their settlement. A standalone host or paper broker has its own declared consumers and support boundary.

Protocol account/source reads in a same-bar paper continuation still reflect the bar's recorded observation. Use the current fill and outcome.args for immediate continuation inputs; do not assume an earlier action refreshed an account source. A spot outcome chain that fires during warm-up is refused: replay the actual entry with startBar: 0 instead of replacing an earlier position with a synthetic fill. Live native protocol outcome chains are refused until the hosted follower can deliver correlated chain-settlement evidence. External hosts can use requestRun today.

Choose the actual primitive actions#

On ftw.trade, get_module_help { id: "aerodrome" } lists every advertised native read/action and imported helper. Use member to inspect one method's argument names, units, required alternatives, scope and docstring. The same information drives editor hover and signature help. There is no runtime aerodrome.help() method. Read the protocol skill for custody, supported markets, pending states and which observation permits the next step.

In another host, inspect that application's capability manifests and registered consumers. A manifest declares an interface; it does not submit transactions or authorize spending. Public Rust functions are not automatically Flow methods. Importing a helper library does not register additional native actions.

Keep user-composable primitives available when publishing a convenience wrapper: the individual swap, approval, supply, withdrawal, stake and claim operations, with their distinct units and settlement boundaries. A wrapper's internal transactions may partially succeed. Preserve each submitted transaction/request identity and confirmed result if a later internal step fails; a thrown exception alone does not establish that nothing happened.

The hosted follower persists structured interruption receipts and checks known pending hashes. It stops for reconciliation after a partial or uncertain batch; it has no automatic rollback or resume command. This is not a durable pre-submission journal: a process crash between a send and checkpoint persistence still requires transaction/account reconciliation. Its interruption hold covers that run's books and market changes. It is not a shared account lease across independent runs using the same wallet.

Sequence on settlement#

These statements request two operations; the second has no success dependency on the first:

await venue.first({ amount: 100 });
await venue.second({ amount: 100 });

await controls strategy pausing at the execution coordinator. It is not an imperative promise binding between adjacent statements. Nest the dependent operation instead:

await venue.first({ amount: 100 }, timeout: "30s")
  .onFill {
    await venue.second({ amount: fill.amountOut }, timeout: "30s")
      .onFill { audit.note({ message: "completed" }); }
      .onRevert {
        await venue.release({ amount: outcome.args.amount }, timeout: "30s")
          .onFill { audit.note({ message: "recovered" }); }
          .onRevert { audit.note({ message: "recovery failed: " + outcome.reason }); }
          .onAbort { audit.note({ message: "recovery refused: " + outcome.reason }); }
          .onTimeout { audit.note({ message: "recovery still pending" }); };
      }
      .onAbort { audit.note({ message: "second refused: " + outcome.reason }); }
      .onTimeout { audit.note({ message: "second still pending" }); };
  }
  .onRevert { audit.note({ message: "first failed: " + outcome.reason }); }
  .onAbort { audit.note({ message: "first refused: " + outcome.reason }); }
  .onTimeout { audit.note({ message: "first still pending" }); };

The names above are illustrative host capabilities. The complete runnable example declares its own host and covers three forward legs with recovery for both reverts and aborts. It uses invented outcomes in an isolated scenario, not recorded or live protocol evidence.

The host must supply the authoritative output amount before advancing a branch that spends fill.amountOut. It must also check token identity, custody, fees, outstanding debt, liquidity and current authorization. A number alone does not establish that two protocols can share that asset. Size an unwind from confirmed positions and the operation's contract; do not withdraw an entire shared account merely because one strategy leg failed. For example, an Aave supply's moved underlying units need not equal the immediately withdrawable aToken balance because scaled balances round. Read the protocol's available balance and sizing contract; all: true is appropriate only when withdrawing that whole position is the intended policy. A reported amount is not a universal spendable claim across actions.

What a handler can inspect#

ReadMeaning
fill.amountOut, fill.priceAuthoritative evidence supplied for this action's fill. Available in .onFill; these describe the operation being handled, not an enclosing action.
outcome.reasonThe host's reason for this action's reported event.
outcome.args.amountThe evaluated original amount of this action, captured when its decision fired. It is not recalculated from current inputs and is not the output of the failed action.
Ordinary declared readsThe observations at the watermark handling the event. They may differ from the observations that triggered the action.

outcome.args has this action's captured evaluated inputs. A capability's manifest selects these with evaluated; contract parameters and built-in evaluated fields also appear in Decision.values. Inspect the compiled action's typed_args for the exact names and program.outcomeArgsSites for the sites whose handlers consume them. Static unevaluated arguments are not available, and unknown fields are compile errors. A nested action's handler has its own outcome scope; an enclosing fill, task result or progress is not an implicit capture. Explicitly pass a value into the child's declared argument when it needs to remain available for the child's recovery. Exact argument types still apply; a Number does not become an exact token-unit integer.

With requestRun, the coordinator takes these arguments from the original request's saved Decision.values. record refuses venue-supplied args: venue evidence cannot replace what the strategy originally requested. The journal retains those captured values across restart.

A raw Run.report host must pass the matching original evaluated values in Outcome.args when a matching handler reads them, and must establish the action/invocation identity itself. Missing or incorrectly typed captured arguments are refused. The site-only raw reporting API is not a durable request coordinator.

Distinguish failure from an unresolved operation#

ObservationPermitted next step
Confirmed fillAdvance its .onFill branch using reconciled outputs and current guards.
Confirmed terminal revertRun .onRevert. Determine which earlier legs still hold assets before choosing compensation.
Pre-submission abortRun .onAbort; no transaction for that primitive was submitted. Earlier filled legs still exist.
Wait timeoutRun .onTimeout for the wait ending. Keep the request and its resources reserved; do not compensate or replace an unresolved order.
Lost acknowledgement, keeper silence or uncertain sendReconcile the original saved submission identity. A missing answer is not a revert or abort.
Partial effects inside a multi-transaction wrapperPreserve every known transaction/result and reconcile the remaining state. Do not collapse the wrapper into an all-or-nothing failure.

requestRun.record accepts terminal fill, revert and abort. Its bounded wait emits wait_timeout internally. The lower-level language also has .onStuck, but requestRun does not accept a stuck observation as a terminal result. A stuck or timed-out request can still settle later. An upstream cancel is a separate operation whose acknowledgement is not proof of a refund; GMX's skill describes its keeper-settled example.

Compensation is an explicitly requested operation that can fail, consume gas, return a different amount or wait on settlement. It is not rollback. Choose the policy yourself: unwind confirmed inventory, keep it and notify an operator, or stop until a reviewed recovery is authorized. Flowscript does not automatically retry failed legs or choose replacement trades.

Own submission and recovery in the host#

Use requestRun for the entire action set participating in a chain. Its default concurrency: "serial" admits one unresolved managed action in that run, including after a wait timeout. Sibling action statements still do not become success-dependent. Use separate success handlers for dependencies.

For each prepared entry, submitRequest exposes three application hooks: build the exact envelope, atomically save its journal and submission identity, and send it through the authorized executor. It saves uncertainty before IO and never retries a failed save or send. On restart restore the whole coordinator snapshot, reconcile uncertain/submitted entries, and deliver terminal evidence by invocation ID. An acknowledgement is not a fill.

Persist incoming evidence, its cursor and coordinator progress consistently. Use single-writer fencing, grants, account/position exclusion and an exact submission identity at the host boundary. A run's serial policy does not exclude a different run from touching the same position. Actions outside the managed action list are returned in own; the application must consume them, including audit/notification decisions and their delivery failures.

Flow libraries currently contain pure helpers and contract interfaces, not effectful functions containing actions and recovery handlers. Users can write the explicit strategy chain, import pure calculations, or package a custom JavaScript/TypeScript host coordinator that consumes the public primitive actions. The extension guide describes that boundary.

Test every exit from the chain#

The example's npm lifecycle tests exercise success, terminal revert and pre-submission abort at every forward leg, failed compensation, snapshot restoration with captured arguments, duplicate old outcomes, and timeout followed by late fill or revert. Add a failure after every internal transaction of a real wrapper, including a failed persistence step or lost acknowledgement. Verify that confirmed partial effects remain visible and uncertain submissions cannot be sent again.

Use .t.flow for input-driven strategy decisions, invariants and explicitly configured paper behavior. Its runner currently cannot inject external keeper settlement or restore requestRun journals. Test those through the embedding API with explicit isolated evidence, then qualify the actual protocol path with its contract/fork tests and authorized real-chain acceptance. Synthetic lifecycle tests establish orchestration behavior; they do not establish a protocol's live execution or economics.