Build libraries and tools
Compose and test your own libraries, declare custom data and actions, and feed recorded external model predictions into an incremental strategy.
The standalone compiler takes a host declaration; it has no dependency on ftw.trade. An application chooses the reads, libraries and actions its programs can use. These extension paths have different responsibilities:
| Extension | Declare | Execute |
|---|---|---|
| Pure language helper | Host.libraries[name], then import "name"; | Flowscript evaluates the imported .flow functions |
| Data or external model output | A typed entry in Host.sources | The application records observations and supplies them to run.step / replay |
| Application tool | A capability manifest with arguments and host authority | The application authorizes and consumes the returned decisions |
| Several providers of an interface | Host.integrations and their served members/markets | The application connects the declared runtime implementations |
Imports provide source functions, not JavaScript callbacks or installed tools. A capability manifest describes an action; declaring it does not implement its consumer, grant signing authority, or authorize network access. Unknown imports, reads and actions are compiler refusals. Use host documentation for source and action descriptions; comments document imported functions for editor help.
Author and compose libraries#
Supply your .flow source under a name in Host.libraries. A library may import other libraries and declare functions or contract interfaces. It cannot declare uses, metrics, triggers or action side effects; the calling program owns its host capabilities. See Libraries and Embedding.
Dependencies form a closure of at most 15 unique libraries, including reached standard libraries. Shared dependencies in a diamond are loaded once. Repeating an import in the same file, an import cycle, an unknown dependency or a duplicate function name is refused. Function names are global and unqualified: import "user/risk"; exposes userCappedEdge(...), not risk.userCappedEdge(...). Choose distinct prefixes for reusable helpers. On ftw.trade, take observations as function arguments rather than hiding source reads in a helper, so the platform can discover the caller's data subscriptions.
Test a library through a small importing program. Resolve and save the exact dependency sources with the program and its compiled language version:
import { loadFlowscript } from '@flowscript/core';
const fs = await loadFlowscript();
const host = { libraries: {
'user/math': 'fn userPositiveEdge(x: number): number { return max(x, 0); }',
'user/risk': `import "user/math";
fn userCappedEdge(x: number, cap: number): number {
return min(userPositiveEdge(x), cap);
}`,
} };
const source = 'import "user/risk"; metric("edge", userCappedEdge(3, 2));';
const resolved = fs.resolveLibraries(source, host);
if (!resolved.ok) throw new Error(JSON.stringify(resolved.problems));
const pinnedHost = { ...host, libraries: resolved.libraries };
const compiled = fs.compile(source, pinnedHost);
if (!compiled.ok) throw new Error(JSON.stringify(compiled.problems));
try {
const tested = fs.test(source, { tests: [{
name: 'cap an imported positive edge', bars: 1, sources: {},
expect: [{ metric: 'edge', at: 'last', equals: 2 }],
}] }, pinnedHost);
if (!tested.ok) throw new Error(tested.error);
if (tested.report.failed) throw new Error(JSON.stringify(tested.report));
const saved = { source, language: compiled.program.language, libraries: resolved.libraries };
// Persist saved with the rest of your host configuration and input cursor.
} finally { fs.drop(compiled.program); }resolveLibraries returns the selected source map without typechecking the strategy against its data host. Compilation and the test still have to pass. When restoring, use the saved map and language, not the current contents of a library registry. The closure includes the exact standard-library copies used by the program. Changed dependency implementations change program identity and cannot silently reuse an incompatible checkpoint.
Built-in learning and external inference#
Built-in ml.* models fit on the program's recorded series in the evaluator. Their training rows, parameters and work participate in its state and budgets; replay and restored live runs use the same implementation. See Trained models for available methods, horizon and retraining semantics. They are bounded rolling models, not an import mechanism for arbitrary Python, ONNX, GPU or JavaScript runtimes.
An external model runs in the application's chosen backend. Publish its output as a typed observation, just as another data source. The runnable external-model.ts example combines that path with an imported freshness helper and a custom research.record action:
cd flowscript/npm
npm run build:wasm
node examples/external-model.tsThe example has no additional dependencies. Its fixed logistic coefficients are illustrative and are not presented as a trained model, profitable strategy or production inference engine. The model document owns feature order, bounds, coefficients and intercept; its SHA-256 identity appears in the program's read key and every recorded output. Inference reads that same document. Replacing the document changes the compiled program identity; an old checkpoint cannot silently resume under the new model.
The application computes a score, records {time, asOf, model, score}, then steps the program. time is when the prediction became available to the strategy; asOf is the input data's time. A response delivered at 110 cannot be inserted into bar 109 merely because its features came from 109. All times in this example are whole evaluation seconds. A future asOf, nonfinite or out-of-range probability, wrong model identity, unsupported feature dimension or feature outside the declared bounds is refused. Unavailable inference is recorded explicitly as score: null.
The strategy checks the model identity and uses the .flow helper to exclude unknown, stale, future and out-of-range observations before emitting a signal. Omission in run.step holds the previous observation, so an outage must send explicit unknown values when holding the old prediction is inappropriate. A freshness guard also limits how long an earlier valid prediction can remain usable.
research.record has a real consumer: it validates the compiled action's host operation and evaluated arguments before appending candidate signals to an application-owned journal. It authorizes no trade. The example validates an entire batch before retaining effects; it does not parse rendered argument IR or report fabricated fills. A real application's storage, effect outbox, permissions, retries and action settlement belong at this boundary.
Reproduce the observations, not the inference request#
Replay the recorded predictions at their original availability watermarks. Do not call a remote model again during replay, substitute a newer model, or backfill historical predictions using features or training data unavailable then. Record model artifact identity, backend/precision configuration, feature schema and input cursor with the inference outputs in a real integration. The example pins a small fixed model; its test history is explicitly synthetic.
Persist the input cursor, observations, evaluator checkpoint and durable effect outbox consistently. A Flowscript checkpoint contains evaluator state; it does not preserve an external model server, in-progress request or model-training state. Asynchronous inference needs the host's own correlated request/result coordinator, recorded delivery and timeout policy. The generic Run.report venue-outcome seam is not a task-result dispatcher.
The example tests exercise actual WebAssembly compilation, replay, live steps, checkpoint restoration, delayed availability, input bounds and the action consumer. There are no latency claims: measure the chosen inference backend, transport and evaluation workload together before selecting a deployment mode.