Skip to content

Lens Instructions

In ConnectedLenses, a lens is not a free-text prompt. It is a typed, versioned operational unit that carries:

  • An input contract — what the engine and renderer will hand the provider.
  • An output contract — what downstream nodes can rely on.
  • A kind — the canonical content type (text, image, video, audio, music, research, pdf, transform, orchestration, validation, routing).
  • A template body with [[label]] placeholders rendered server-side via fn_render_version_body.
  • A set of parameters that bind labels to typed lenses.tools rows.
  • A version with a published-state lifecycle.

This is the seam where prompt engineering becomes reusable capability. A lens published once can be bound to any agent, embedded in any workflow node, and called from CLI, API, or UI without re-typing its prompt.

Anatomy

Lens record

lenses.lenses holds the author-owned envelope: title, description, content (legacy free-form), visibility, status, tags, params (legacy). The authoritative behavior lives on the version row.

Lens version

lenses.versions stores the executable definition. A lens has many versions; the head pointer (lenses.lenses.head_version_id) names the active one. Status is 'draft' | 'published' | 'archived'.

Key columns:

ColumnPurpose
template_body[[label]]-token body rendered by fn_render_version_body. Labels may contain spaces and hyphens (e.g. [[Visual Tone]]). Append ! to mark optional: [[Style Notes!]].
input_contractTyped contract for inputs. Nullable for backward compat.
output_contractTyped contract for outputs. Required kind and artifactKind when present.
published_atTimestamp of first publication of this version.

Parameters

lenses.version_parameters binds each [[label]] placeholder to a lenses.tools row. The tool column on the param resolves the rendering type (text, textarea, json, number, select, url, date, file, etc.) and validation rules.

Label syntax (parsed by extractParams in libs/utils/text):

  • Single or multi-word: [[Topic]], [[Visual Tone]], [[Target Audience]]
  • Underscores and hyphens: [[word_count]], [[source-language]]
  • Optional marker: [[label!]] — trailing ! is stripped from the stored label and signals the parameter is not required

TypeScript shapes: LensVersionParam, ToolRecord.

Contracts

Defined in libs/types/src/lib/contracts.types.ts. Stored as JSONB on the version row.

LensKind

ts
type LensKind =
  | 'text' | 'image' | 'video' | 'audio' | 'music'
  | 'research' | 'pdf' | 'transform'
  | 'orchestration' | 'validation' | 'routing'

The kind names the content domain the lens operates in. The execution engine uses it to pick artifact storage, render UI affordances, and validate envelope shape. See contracts.types.ts:18.

LensInputContract

What the engine guarantees to pass into the provider call.

ts
interface LensInputContract {
  kind: LensKind
  fields: Record<string, ContractFieldSchema>
  requireAnyOf?: string[][]  // OR-of-AND: at least one group satisfied
  strict?: boolean           // reject extra fields
}

ContractFieldSchema covers string / number / integer / boolean / url / json / array / any with min, max, pattern, enum, itemType. See contracts.types.ts:34.

LensOutputContract

What the lens promises on success.

ts
interface LensOutputContract {
  kind: LensKind
  artifactKind: ArtifactKind     // from execution.types
  outputType?: string             // e.g. 'pdf'
  schema?: Record<string, ContractFieldSchema>
  tokens?: string[]               // exposed source-output keys; defaults to ['output']
  containsSensitive?: boolean
}

NodeOutputEnvelope

The runtime envelope returned by every provider and passed between workflow nodes. output is always a string projection (so [[label]] rendering keeps working in downstream nodes); data carries the structured fields. See contracts.types.ts:99.

Validation

The engine validates twice per node:

  1. Pre-call — input fields against LensInputContract.fields. If strict, extra keys reject.
  2. Post-call — provider envelope against LensOutputContract.schema. Failure emits contract.violated (WorkflowEventType.CONTRACT_VIOLATED) and counts toward retry budget.

Lifecycle

Forking a published version creates a new draft row sharing lens_id, with a fresh version_number and parent_version_id pointing at the source.

When to author a new lens vs. a new version

SituationAction
Same task, refined promptNew version
Same task, contract change (added required field)New version (breaking → bump version_number major)
Different task, related domainNew lens
Different output kindNew lens

How agents bind lenses

agents.lens_bindings (legacy, see agents.types.ts:66) ties a lens to an agent with an optional version pin. When the binding pins a version_id, the agent always uses that version even if the lens publishes a new head.

For team-level execution, a lens is referenced indirectly: the workflow node binds a lens_version_id, the team is assigned to the workflow via agents.workflow_assignments, and a team member runs the node with their agents.tool_profiles allowlist applied.

Future work

Proposed (not yet implemented): an instruction_category column on lenses.versions.

Today, kind tags content modality. There is no first-class column tagging the instruction role of a lens within an agent pipeline. The proposed enum:

ValuePurpose
instructionRole / persona / behavior policy
researchWeb research, source analysis, citation extraction
planningTask decomposition, milestone planning, dependency mapping
generationText / image / video / audio / code generation
validationOutput checking, schema validation, factual review, safety review
routingAgent selection, model selection, branching
memoryContext loading, prior-execution retrieval, preference retrieval
exportPDF / Markdown / JSON / API payload export

Migration sketch (not yet committed):

sql
ALTER TABLE lenses.versions
  ADD COLUMN instruction_category text;

ALTER TABLE lenses.versions
  ADD CONSTRAINT versions_instruction_category_check
  CHECK (instruction_category IS NULL OR instruction_category = ANY (ARRAY[
    'instruction', 'research', 'planning', 'generation',
    'validation', 'routing', 'memory', 'export'
  ]));

This unblocks capability-driven node assignment in workflow-execution.md without scraping tags.