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

Use strategy outputs

Consume evaluated metrics, text and action intents in charts, application logs and your own data consumers, with delivery and storage owned by the host.

A program's outputs can become data in your own application: chart series, status labels, event rows, alerts or inputs to another consumer. The core returns structured values. Your host chooses where they go and which consumers may read them.

What comes out#

OutputCore APIExample consumer
Numeric or boolean metricsstep.metricsA chart, table, monitor or derived dataset
Text metricsstep.textsA status label, annotation or application log message
Action decisionsstep.decisionsAn event stream, notification adapter or execution adapter
Application logsRecords your application constructsA log viewer or observability service
Stored rowsRecords your application persistsA database, API, dashboard or another application

metric("average", average) publishes a number. metric("state", "ready") publishes text, returned through texts. In a replay those fields contain one column per metric; run.step returns the values for the bar just evaluated.

An action decision means the program requested a declared action. It is not a receipt, fill or confirmation that an external effect happened. An application can inspect or store decisions without attaching an execution adapter.

Turn evaluated values into application data#

The following complete example computes an average, publishes a text state and requests a host-defined signals.publish action when the condition changes. Its six hourly prices are an illustrated input sequence, not recorded market data. The example collects outputs in memory; it does not send notifications, place orders or write a database.

Use an authorized source build of the package as described in Embedding Flowscript. Save this as outputs.mjs in an application with that built package installed and run node outputs.mjs.

TypeScript
import { loadFlowscript } from '@flowscript/core';

const flowscript = await loadFlowscript();
const host = {
  sources: [
    { name: 'bars.close', type: 'number', keyed: true, intervals: ['1h'] },
  ],
  capabilities: [JSON.stringify({
    name: 'signals',
    actions: {
      publish: {
        args: { message: 'Text', close: 'Number' },
        required: ['message', 'close'],
        evaluated: ['message', 'close'],
        authority: { kind: 'host', operation: 'signals.publish' },
      },
    },
  })],
};
const source = `flowscript "0.3";
uses { bars: "^1", signals: "^1" }
const close = bars.close("DEMO-USD", "1h");
const average = isNa(close[2]) ? na : sma(close, 3);
const above = close > average;
metric("close", close);
metric("average", average);
metric("state", isNa(average) ? "warming up" : above ? "above average" : "at or below average");
when (above) { signals.publish({ message: "above average", close: close }); }
when (!above) { signals.publish({ message: "at or below average", close: close }); }
`;
const compiled = flowscript.compile(source, host);
if (!compiled.ok) throw new Error(JSON.stringify(compiled.problems));

try {
  if (compiled.program.warnings.length) {
    throw new Error(JSON.stringify(compiled.program.warnings));
  }
  const started = flowscript.run(compiled.program);
  if (!started.ok) throw new Error(started.error);
  try {
    const run = started.run;
    if (run.unmodelled.length) throw new Error(run.unmodelled.join('; '));
    const closes = [100, 102, 104, 101, 99, 105];
    const firstClose = Date.UTC(2026, 0, 1, 1) / 1000;
    const runId = 'illustrated-signal-run';
    const chartRows = [];
    const actionIntents = [];
    const applicationLogs = [];
    // Preserve unknown values as explicit chart gaps when encoding JSON.
    const chartNumber = (value) => {
      if (Number.isNaN(value)) return null;
      if (!Number.isFinite(value)) throw new Error('Invalid output metric');
      return value;
    };

    for (let i = 0; i < closes.length; i++) {
      const time = firstClose + i * 3600;
      const step = run.step({ 'bars.close': closes[i] }, time);
      if (!step.ok) throw new Error(step.error);
      if (step.orders.length || step.refused.length) {
        throw new Error('Unexpected broker order or invariant refusal');
      }

      chartRows.push({
        time,
        close: chartNumber(step.metrics.close),
        average: chartNumber(step.metrics.average),
        state: step.texts.state,
      });
      for (const decision of step.decisions) {
        actionIntents.push({ runId, bar: step.bar, time, decision });
      }
      // This log record belongs to the application, not a core stdout stream.
      applicationLogs.push({
        level: 'info',
        event: 'flowscript.step',
        runId,
        bar: step.bar,
        time,
        message: step.texts.state,
        actionCount: step.decisions.length,
      });
    }

    console.log(JSON.stringify({ chartRows, actionIntents, applicationLogs }, null, 2));
  } finally {
    started.run.drop();
  }
} finally {
  flowscript.drop(compiled.program);
}

The program explicitly waits for three observations with isNa(close[2]), so its average is unknown for the first two bars, then 102, 102.333…, 101.333… and 101.666…. A chart can draw close and average against time, leave the initial unknown values as gaps, and use state for labels. time is the input bar's close in Unix seconds; convert it explicitly if your visualization expects milliseconds.

The three actual decisions occur at bars 2, 3 and 5. Each retains the core's site, capability, action, symbolic args and evaluated values. Here, values.message and values.close contain the text and price at the moment the action fired. A consumer can place markers on those bars or store them as events. These records do not claim that anything was delivered or executed.

Where logs come from#

The core returns metrics, text, decisions and result errors; this example does not call a Flowscript log function or receive a core stdout stream. The JavaScript application creates applicationLogs from each successful step, then prints its collected data with its own console.log.

A host can instead send those structured records to its own logging service, store text metrics as annotations, or declare a separate logging capability. In each case the host implements delivery and chooses retention. Compiler diagnostics and runtime errors should remain visible as failures, not successful evaluation log entries.

Connect a durable consumer#

Choose the output contract your consumers need: program identity and version, run identity, source identity and units, input cursor, evaluation timestamp, metric names, and action site. Persist that identity with the output so a replay can be recognized rather than inserted as a new event.

Commit output rows, the input cursor and the next checkpoint together where your storage supports a transaction. For delivery to another system, an application-owned durable outbox can carry committed events; the consumer needs an idempotency key that identifies the same run, input and action site. An in-memory array like this example has no durability or delivery guarantee.

An adapter that performs an external action must retain and reconcile its real outcome. run.report can feed supported outcomes back to the run for handlers; an emitted decision alone must never be marked as a receipt. The language does not provide your database, charting library, event bus, signing service or consumer access policy.

These boundaries also let an application publish strategy outputs as its own data product or use them as inputs to another process. The host owns the schema, permissions, delivery and storage. For ftw.trade's particular strategy and visualization workflow, read its platform documentation; for the underlying API, read the language reference.