@clxmedia/cliq-flow
v1.0.11
Published
CLiQ Flow Engine
Readme
Usage
FlowEngine is a lightweight, data‑only workflow helper. It takes:
- A "template" (graph definition) describing nodes and their downstream relationships.
- A collection of runtime "items" (any shape with at least a
slugstring) representing the concrete work objects.
From those it can:
- Build a full flow instance (
itemsToFlowInstance) with status for every template node (real or mocked if missing). - Determine which items are now activatable (
getItemsToActivate). - Determine which completed items must be reset when restarting (
getItemsToReset). - Tell if the flow can be reset from the perspective of a given task slug (
canResetFlow).
Item Agnostic by Design
FlowEngine<TItem> is generic. It does not care if your items are CLX tasks, tickets, jobs, forms, or custom domain objects—so long as each item exposes a slug (used to line up with the template).
Status detection is the only behavioral coupling. You supply (or let the engine auto‑discover) a detectStatus(item) -> 'pending' | 'open' | 'closed' function. If you omit one, the engine runs a small predicate list (FlowItemStatusDetectorMappings) to guess the status; today it ships with a legacy CLXTask detector keyed off is_active / closed_at.
If none match, status defaults to pending.
Core Concepts
| Concept | Summary |
| ------- | ------- |
| Flow Template | Static directed graph keyed by internal node ids. Each node holds a slug, list of downstream node ids, and optional joinCondition (AND or OR). |
| Node Slug | The external business identifier; must match the slug on your runtime items. |
| Join Condition | Governs activation: AND = all predecessors must be closed; OR = any predecessor closed. Missing value defaults to AND. |
| Restart Node | Special node id (restartNodeId) that acts as the anchor for reset operations. |
| Flow Instance | Runtime projection created from template + items (real + any mock placeholders for missing slugs). |
Status Semantics
pending -> Not yet active (eligible for activation once dependencies satisfied).
open -> In progress (active but not yet completed).
closed -> Completed / finished.
You control the mapping by providing an explicit detector:
const engine = new FlowEngine<MyItem>(template, item => {
if (item.doneAt) return 'closed';
if (item.startedAt) return 'open';
return 'pending';
});Or rely on built‑ins (for legacy CLXTask shape):
const engine = new FlowEngine<CLXTask>(template); // auto-detects via is_active / closed_atDefining a Flow Template
import { FlowEngine } from 'cliq-flow';
import type { FlowTemplate } from 'cliq-flow/types';
const onboardingTemplate: FlowTemplate = {
nodes: {
share: { slug: 'share_onboarding_form', downstream: ['grant'] },
grant: { slug: 'grant_access_gbp_manager', downstream: ['create'] },
create: { slug: 'create_gbpa_ads', downstream: ['review'] },
review: { slug: 'review_gbpa_ads', downstream: ['launch'] },
launch: { slug: 'launch_gbpa_ads', downstream: [] },
},
startNodes: ['share'],
restartNodeId: 'share',
restartNodeLabel: 'Restart Onboarding',
};Node ids (share, grant, etc.) are internal; slugs connect to item data.
Using with CLXTask (Example Only)
import { FlowEngine } from 'cliq-flow';
import { cliqTaskLibrary } from './fixtures/tasks'; // sample array of CLXTask
const engine = new FlowEngine(onboardingTemplate); // auto status detection
const activatable = engine.getItemsToActivate(cliqTaskLibrary);
// -> tasks currently in 'pending' whose predecessors are satisfied
const flowInstance = engine.itemsToFlowInstance(cliqTaskLibrary);
// flowInstance.nodes[nodeId].status holds evaluated status
const canReset = engine.canResetFlow('create_gbpa_ads');
// { canReset: true | false, resetLabel?: string }
const toReset = engine.getItemsToReset(cliqTaskLibrary);
// -> closed successor tasks of the restart nodeUsing with ANY Other Item Type
Provide a detector if your shape differs:
interface Job { slug: string; started?: string; finished?: string; }
const jobDetector = (job: Job) => job.finished ? 'closed' : job.started ? 'open' : 'pending';
```ts
const jobEngine = new FlowEngine<Job>(onboardingTemplate, jobDetector);That is enough; the engine only reads slug and the detector result.
FlowEngineWrapper
FlowEngineWrapper is a higher-level abstraction that manages multiple FlowEngine instances, each corresponding to a different flow template. It allows you to work with a collection of templates and perform operations across all of them or target a specific template by its GUID.
Key Features
- Multi-Template Management: Handles an array of
FlowTemplateobjects, creating a dedicatedFlowEnginefor each. - Aggregated Operations: Methods like
getItemsToActivateandgetItemsToResetcan aggregate results from all templates or filter by a specific template GUID. - Template Selection: Pass an optional
templateGuidto operations to focus on a single template, or omit it to process all templates.
Usage Example
import { FlowEngineWrapper } from 'cliq-flow';
import type { FlowTemplate } from 'cliq-flow/types';
const templates: FlowTemplate[] = [
onboardingTemplate,
approvalTemplate,
// ... more templates
];
const wrapper = new FlowEngineWrapper<MyItem>(templates, customDetector);
// Get activatable items across all templates
const allActivatable = wrapper.getItemsToActivate(items);
// Get activatable items for a specific template
const specificActivatable = wrapper.getItemsToActivate(items, 'onboarding-template-guid');
// Get items to reset across all templates
const allResettable = wrapper.getItemsToReset(items);
// Get items to reset for a specific template
const specificResettable = wrapper.getItemsToReset(items, 'approval-template-guid');This wrapper is particularly useful when your application needs to handle multiple workflow types simultaneously, providing a unified interface for activation and reset operations.
What Happens If Items Are Missing?
That is enough; the engine only reads slug and the detector result.
If a template node has no matching item, the engine fabricates a lightweight mock (id = -1) so the graph stays complete. Its status is inferred from a predecessor (if one exists) or defaults to pending. This lets downstream activation logic remain stable even with partial data loads.
Activation Logic Recap
An item is returned from getItemsToActivate when:
- Its current status is
pending. - It has no predecessors (start node) OR its predecessors satisfy the join condition.
Reset Logic Recap
getItemsToReset performs a breadth‑first traversal starting from restartNodeId and collects downstream nodes that are presently closed. Those items should be transitioned back to an earlier state by your application when a restart is initiated.
Reset Eligibility
canResetFlow(taskSlug) returns { canReset: boolean; resetLabel?: string } and is true only when the task slug maps to a node that is a successor (not the restart node itself) of restartNodeId in the template graph.
Minimal Integration Steps
- Model your template graph (
FlowTemplate). - Decide on / implement status detection.
- Instantiate
FlowEnginewith template (+ optional detector). - Feed runtime items to
itemsToFlowInstance/getItemsToActivate/getItemsToResetas needed. - Use returned statuses to drive UI / orchestration.
Error Handling & Edge Cases
- Null / undefined item passed to detector -> treated as
pending. - Detector exceptions inside auto‑mapping predicates are swallowed and the next predicate is tried.
- Unknown item shape without custom detector -> always
pendinguntil you supply logic. - Cycles in the template are not explicitly guarded; provide an acyclic graph for predictable activation.
Extending Auto Detection
Add a new predicate + detector pair to FlowItemStatusDetectorMappings (PR welcome) so future consumers get zero‑config support for your item type.
FlowItemStatusDetectorMappings.push({
predicate: (item) => 'startedAt' in item && 'finishedAt' in item,
detector: (item) => item.finishedAt ? 'closed' : item.startedAt ? 'open' : 'pending'
});TL;DR
FlowEngine = (Template Graph + Items + Status Detection) -> Activation + Reset Insights. The engine is item‑agnostic; just give it a slug and a way to detect status.
Need more examples? Check the test fixtures under src/__tests__.
Change Log
See Changelog for more information.
Author
Brandon Thompson
License
Licensed under the MIT License - see the LICENSE file for details.
Development
Install dependencies and build:
yarn install
yarn buildTesting
This package now uses Vitest for unit tests.
Common commands:
yarn test # run test suite once
yarn test:watch # watch mode
yarn test:cov # with coverage (./coverage)Test files live under src/__tests__ and use the *.spec.ts suffix.
