npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

@zflo/core

v0.3.0

Published

Core package for ZFlo flow execution framework

Readme

@zflo/core

Core package for ZFlo flow execution framework.

  • UI-agnostic engine and types
  • Deterministic state management and rule evaluation (CEL by default)
  • Event-driven execution and history tracking

Install

pnpm add @zflo/core

Quick start (branching + state)

import { FlowEngine, type ZFFlow } from '@zflo/core';

// Branching flow with state and CEL conditions
const flow: ZFFlow = {
  id: 'dragon-quest',
  title: 'Dragon Quest',
  startNodeId: 'start',
  expressionLanguage: 'cel',
  globalState: { flags: { hasSword: false } },
  nodes: [
    {
      id: 'start',
      title: 'Village Gate',
      content: 'You stand at the village gate. A dragon threatens the land.',
      outlets: [{ id: 'to-decision', to: 'decision', label: 'Continue' }],
    },
    {
      id: 'decision',
      title: 'Prepare for Battle',
      content: 'Will you gather a weapon or charge in?',
      outlets: [
        {
          id: 'fight-now',
          to: 'fight',
          label: 'Fight the dragon',
          // Only enabled when you already have a sword
          condition: 'flags.hasSword == true',
        },
        {
          id: 'find-sword',
          to: 'armory',
          label: 'Find a sword first',
          // Default path when condition above is false
        },
      ],
    },
    {
      id: 'armory',
      title: 'Village Armory',
      content: 'You obtain a sword.',
      // Set some state when entering this node
      actions: [{ type: 'set', target: 'flags.hasSword', value: true }],
      outlets: [{ id: 'back-to-decision', to: 'decision', label: 'Return' }],
    },
    {
      id: 'fight',
      title: 'Dragon Lair',
      content:
        'You confront the dragon with your ${flags.hasSword ? "shiny sword" : "bare hands"}.',
      outlets: [{ id: 'end-victory', to: 'victory', label: 'Strike!' }],
    },
    {
      id: 'victory',
      title: 'Victory',
      content: 'The dragon is defeated. The village is safe!',
      // End nodes have no outlets; type is inferred as "end"
    },
  ],
};

const engine = new FlowEngine(flow);

// Step 1: start the flow
let res = await engine.start();
console.log(res.node.node.title); // "Village Gate"
console.log(res.choices.map((c) => c.label)); // ["Continue"]

// Step 2: move to decision (single outgoing path)
res = await engine.next(res.choices[0].id);
console.log(res.node.node.title); // "Prepare for Battle"
console.log(res.choices.map((c) => c.label)); // e.g., ["Find a sword first"] (fight disabled until hasSword)

// Step 3: choose to find a sword
res = await engine.next('find-sword'); // use the choice id (outlet id)
console.log(res.node.node.title); // "Village Armory"
console.log(res.state.flags); // { hasSword: true }

// Step 4: go back to decision, now "Fight" becomes available
res = await engine.next('back-to-decision');
console.log(res.node.node.title); // "Prepare for Battle"
console.log(res.choices.map((c) => c.label)); // ["Fight the dragon"]

// Step 5: fight and finish
res = await engine.next('fight-now');
console.log(res.node.node.title); // "Dragon Lair"
res = await engine.next('end-victory');
console.log(res.node.node.title); // "Victory"
console.log(res.isComplete); // true

Key concepts

  • State is persisted and used to auto-select paths when conditions are met
  • CEL expressions can be used in rules and conditions
  • Works with multiple input formats via adapters (e.g., Mermaid)

Execution model

  • Engine: new FlowEngine(flow, options?)
  • Start and step:
    • await engine.start() → returns ExecutionResult
    • await engine.next(choiceId?) → advance via a selected outlet id
  • Result shape matches ExecutionResult:
    • node is an AnnotatedNode → access node via res.node.node
    • choices are available outlets with labels and outletId
    • state is the current state snapshot
    • isComplete indicates arrival at an inferred end node

Data model (types)

From src/types/flow-types.ts:

export interface ZFFlow {
  id: string;
  title: string;
  description?: string;
  expressionLanguage?: 'cel';
  globalState?: Record<string, unknown>;
  stateSchema?: JSONSchema7; // optional JSON Schema validation
  stateRules?: StateRule[]; // optional rule engine
  autoAdvance?: 'always' | 'default' | 'never';
  metadata?: Record<string, unknown>;
  nodes: ZFNode[];
  startNodeId: string;
}

export interface ZFNode {
  id: string;
  title: string;
  content?: string; // supports ${...} interpolation
  actions?: StateAction[]; // executed on node enter
  outlets?: XFOutlet[]; // edges
  autoAdvance?: 'always' | 'default' | 'never';
  metadata?: Record<string, unknown>;
}

export interface XFOutlet {
  id: string; // used as choiceId
  to: string; // target node id
  label?: string;
  condition?: string; // CEL by default
  actions?: StateAction[]; // executed when traversed
  metadata?: Record<string, unknown>;
}

export interface StateAction {
  type: 'set';
  target: string; // e.g., flags.hasSword
  expression?: string; // e.g., true
}

Choices and disabled states

res.choices are derived from the current node's outlets:

  • When options.showDisabledChoices is false (default), only enabled outlets appear.
  • When true, disabled choices include disabled: true and disabledReason.
  • Single enabled outlet may be labeled "Continue" and include a helpful description.

Access the outlets via choice ids:

const choices = res.choices; // Choice[]
await engine.next(choices[0].id); // id equals outlet id

Auto-advance

Control via node, flow, or engine options (autoAdvance: 'always' | 'default' | 'never'):

  • always: engine selects the first matching conditional outlet (if/else logic), otherwise the default outlet (without condition).
  • default (and never for decisions): no automatic transition from decision nodes.

Events

Engine emits typed events (see src/types/execution-types.ts):

engine.on('nodeEnter', ({ node, state }) => {
  /* ... */
});
engine.on('nodeExit', ({ node, choice, state }) => {
  /* ... */
});
engine.on('stateChange', ({ oldState, newState }) => {
  /* ... */
});
engine.on('autoAdvance', ({ from, to, condition }) => {
  /* ... */
});
engine.on('complete', ({ history, finalState }) => {
  /* ... */
});
engine.on('error', ({ error, context }) => {
  /* ... */
});

Interpolation in titles and content

Content supports ${...} expressions evaluated against the current state via the content interpolator. Escape with \${...} to render literally.

// Example content
content: 'You confront the dragon with your ${flags.hasSword ? "shiny sword" : "bare hands"}.';

History and state APIs

  • engine.getCurrentNode()AnnotatedNode | null
  • engine.getHistory()ExecutionStep[] (includes state snapshots)
  • engine.getAvailableChoices()Choice[]
  • engine.getState() → current state
  • engine.canGoBack() / engine.goBack()
  • engine.reset() → reset to globalState