Skip to content

Open Source Workflows

LenserFight Community Edition ships a multi-step, DAG-based workflow engine that connects versioned Lenses through typed IO contracts. This document is the single source of truth for how the system is organised, what guarantees it provides, and how contributors should extend it.

It is written to support nine concrete lens kindstext, image, video, research, pdf, transform, orchestration, validation, routing — and the twenty-five supported workflow tasks enumerated at the end.

Layered architecture

The codebase follows the Nx layer direction enforced by eslint.config.js:

text
apps/web (composition root)


libs/features/workflows · libs/features/lenses · libs/features/lens-kinds


libs/domain · libs/data · libs/api · libs/infra/execution · libs/infra/moderation · libs/infra/storage · libs/infra/providers


libs/shared · libs/ui · libs/utils


libs/types

Every workflow change must respect these boundaries. Orchestration belongs in libs/infra/execution; presentation belongs in libs/features/workflows; contracts and DTOs belong in libs/types.

Core concepts

ConceptDescriptionPrimary table
LensVersioned prompt template with typed input and output contractslenses.lenses, lenses.versions
Lens KindClassifier describing the media modality and role of a lenscontent.tag_map with kind:* tag
ParameterTyped slot filled at execution time from user input or upstream node outputlenses.version_parameters joined to lenses.tools
WorkflowDAG of lens nodes connected by edgeslenses.workflows
NodeA single lens invocation inside a workflow, with its own config overrideslenses.workflow_nodes
Edgesource_output_keytarget_param_label mapping with a merge strategylenses.workflow_edges
RunOne execution of a workflow with root inputs, status, and budgetlenses.workflow_runs
Node ResultPer-node status, output envelope, tokens, cost, timinglenses.workflow_node_results
Artifact (Ray)Durable output of a single lens run (text, json, image, video, pdf)execution.artifacts joined to media.objects

Typed IO contracts

Every published lens version carries two optional contracts:

  • lenses.versions.input_contract — describes the required and optional parameters, their kinds, and validation rules.
  • lenses.versions.output_contract — describes the envelope the engine must produce on success.

The envelope is the runtime shape passed between nodes:

ts
type NodeOutputEnvelope = {
  kind: LensKind                     // 'text' | 'image' | 'video' | 'research' | 'pdf' | ...
  artifact_kind: ArtifactKind        // 'text' | 'image' | 'audio' | 'video' | 'json' | ...
  output: string                     // canonical text projection (always present)
  data?: Record<string, unknown>     // structured fields matched against output_contract.schema
  media?: { url: string; mime?: string; width?: number; height?: number; duration_s?: number }
  metadata?: Record<string, unknown> // non-normative metadata (model, latency, tokens, etc.)
}

Contracts are validated in two places:

  1. Before provider call — the engine checks that the node's rendered inputs satisfy its input_contract.
  2. After provider call — the engine validates the provider response against output_contract before writing workflow_node_results.output_data and before exposing it to downstream nodes.

Contract failures are surfaced as contract_violated events and may be retried according to config.retry.

Lens kinds

KindPurposeTypical artifactDefault providers
textBlog posts, reports, summaries, docs, captionstextopenai, anthropic, google, mistral, ollama
imageConcept art, thumbnails, product visuals, brandingimagefal-ai, openai (dall-e)
videoShort-form videos, storyboards, scene sequencesvideofal-ai
researchDeep search + synthesis, research packetsjson (findings + sources)platform retrieval + any text provider
pdfDocument export of text or research outputsjson with output_type='pdf' + media.objectsPdfExportProvider
transformReshapes an envelope (text → prompt, research → slides)matches inputtext providers
orchestrationPlans other lens calls, manages statejson plantext providers
validationScores an output against criteria, returns pass/fail + reportjsontext providers
routingClassifies user intent, selects a downstream branchjsontext providers

See create-a-lens-kind for authoring new kinds.

Execution model

The engine is a Kahn topological scheduler that runs nodes in waves: each wave is a layer of the DAG where every member has zero remaining in-degree. Within a wave, nodes execute with Promise.all. Across waves, execution is strictly sequential.

Guarantees

  • No cycles: enforced at edit time via WorkflowExecutionService.detectCycle and at run time by refusing to schedule.
  • No double-trigger: workflow_runs carries an idempotency_key derived from (workflow_id, rootInputsHash).
  • Deterministic fan-in: each edge carries an explicit merge strategy. The default is last_write_wins to preserve historical behaviour; new nodes should pick an explicit strategy.
  • Deterministic cancellation: stopExecution aborts the AbortController; in-flight nodes become cancelled; pending nodes are marked cancelled via markRemainingCancelled.

Failure propagation

Each node declares an on_parent_failure policy on its config:

PolicyBehaviour
skipNode is marked skipped; its own dependents inherit the same policy decision. (Default for new nodes.)
propagateNode is marked failed with error_message: "upstream_failure".
substitute_defaultMissing upstream values are substituted with the empty string. (Legacy behaviour.)

Retry, timeout, cancellation

Per-node config:

ts
config.retry = {
  attempts: 3,
  backoff_ms: 500,           // exponential with jitter
  retry_on: ['timeout', 'provider_error', 'rate_limit']
}
config.timeout_ms = 60000

Cancellation is cooperative: the engine passes an AbortSignal into every provider call. Providers that respect abort (fetch-based) return early; others are bounded by timeout_ms.

Fan-in merge strategies

When two or more edges target the same target_param_label on the same node, the target node's config.merge (or the edge's merge_strategy) decides the outcome:

StrategyResult
last_write_winsThe last edge in edges array order replaces the value.
concatValues are joined with \n\n.
arrayValues are wrapped as a JSON array and rendered as JSON.
json_objectValues are collected into a JSON object keyed by the source node label.

Moderation gateway

The engine calls ModerationGateway.check(envelope, policy) before the provider call (on rendered input) and after the provider call (on the output envelope). Policy is per-node:

ts
config.moderation = 'off' | 'input' | 'output' | 'both'

Default implementation is the already-built ContentModerationService in libs/infra/moderation. A violation marks the node failed with error_message: "moderation_blocked" and writes an execution.execution_tags row with tag = 'policy_violation'.

Provider dispatch

Providers are registered in libs/infra/execution/src/lib/execution.registry.ts under canonical keys: openai, anthropic, google, mistral, ollama, fal-ai. The browser executor picks a provider based on the lens's output_contract.kind plus the funding source:

Funding sourcePathNotes
user_byok_localDirect fetch, AES-GCM key from IndexedDBNo key ever leaves the browser
user_byok_cloudDelegates to the Cloud Worker pathCloud edition only
platform_creditWallet-backed execution via walletApiClientCommunity and Cloud
sponsoredSame path as platform_credit with sponsor_idCloud only

The 25 supported tasks

#TaskOwning kind(s)
1Text generation lens for articles, reports, poststext
2Image generation lens from a structured visual briefimage
3Video generation lens from script + storyboard inputvideo
4Lens chains that connect multiple lenses into one resultany + orchestration
5Workflows with sequential and parallel-safe stepsany
6Validate concurrent workflow steps for race-condition safetyvalidation
7Deep-search lens that gathers and synthesises researchresearch
8PDF creation lens converting research into final documentspdf
9Routing lens that picks the correct media workflowrouting
10Prompt-planning lens that turns intent into executable stepsorchestration
11Refinement lens improving weak outputs without changing intenttransform
12Validation lens for formatting, completeness, schema correctnessvalidation
13Orchestration lens managing multi-step executionorchestration
14Reusable templates for common lens patternsseed pack (Phase 4)
15Fallback logic for failed steps or incomplete outputsengine retry + on_parent_failure
16Summarisation lens for long research or large outputstext + transform
17Transformation lens converting text → image prompttransform
18Transformation lens converting text → video scenestransform
19Transformation lens converting research → PDF-ready sectionstransform
20Style-control lens applying tone/brand/visual consistencytransform
21Evaluation lens scoring output quality against requirementsvalidation
22Merge step combining outputs from multiple branchesengine merge strategies
23Input and output schemas for all major lens typesinput_contract/output_contract
24Workflow test plan covering edge cases and concurrencytest-plan.md
25Scalable architecture supporting future parallel chainsPhase 6 observability + idempotency