Template Authoring Guide
This guide describes how to author a public LenserFight template — a Lens, Workflow, Battle, or Agent — that ships with the platform's seed data and lives in the portable .lenserfight/ tree.
Before you start, read
docs/reference/storage-architecture.mdso you understand the portable-vs-runtime separation.
Reserved authors
Every public template is authored by one of three reserved accounts:
| Handle | Used for | |
|---|---|---|
@lenserfight | [email protected] | Default author for every public template |
@chainabit | [email protected] | Productivity / startup-ops templates (the BUILD half) |
@conectlens | [email protected] | Community-facing lenses (Thread Starter, Challenge Creator) |
If your template doesn't belong to @chainabit or @conectlens, the author is @lenserfight.
Two-step authoring
A template lives in two synced places:
- The
.lenserfight/tree — human-readable, markdown with YAML frontmatter, reviewed in PRs. - The
supabase/seeds/4*_*templates.sqlfiles — the deterministic-UUID SQL that populates the database.
Both must stay in sync. A pgTAP test fails if a slug exists in one place but not the other.
Step 1 — Pick a slug
Slugs follow the content.tags.slug constraint:
- Lowercase
- 2–40 characters
^[a-z0-9]+([\-][a-z0-9]+)*$
Once published, a slug is immutable. Renaming it breaks /ray/<slug> links and every shared template URL.
Step 2 — Write the LENS.MD (or AGENT.MD, etc.)
Create the file at .lenserfight/lenses/<slug>/LENS.MD with this frontmatter:
---
name: <slug>
title: <Human-readable title>
description: <One-sentence what-and-why, ≤ 160 chars. Used by /ray search.>
author: '@lenserfight' # or @chainabit / @conectlens
visibility: public # public | unlisted | private
forkable: true # whether fn_lens_create can clone it
disclaimer: | # MANDATORY for legal/finance templates
<full disclaimer text>
inputs:
- label: <param_label>
tool: text | textarea
required: true
example: <one-line example>
outputs:
kind: text | image | video | audio | table | checklist | script | structured
rays: [<slug>, <slug>, …] # at least one of the canonical rays
seed_uuid: <deterministic UUID> # MUST match the SQL seed
---
# <Title>
<body — the actual prompt template using [[param_label]] for substitution>The body is the prompt the model sees. Reference parameters with [[param_label]] (the bracket-bracket syntax mirrors the [[:uuid]] form the database stores after slug→UUID rewriting).
Step 3 — Add the SQL seed block
Open the matching seed file under supabase/seeds/:
| Category | Seed file |
|---|---|
| Chain primitives (intent, plan, refine) | 40_lens_chain_templates.sql |
| Developer / community lenses | 41_developer_lens_templates.sql |
| Creator / business / legal / startup | 45_creator_business_lens_templates.sql |
| Chainabit productivity | 47_chainabit_productivity_templates.sql |
| Workflows | 42_production_workflow_templates.sql / 48_workflow_templates.sql |
| Battles | 05_battles.sql / 51_lenser_character_battles.sql |
Pick a deterministic UUID following the convention used in the file (e.g. 45000000-0001-LLLL-0001-… for creator lenses). Reuse the same UUID in the frontmatter's seed_uuid field.
The minimum SQL block for a lens:
IF NOT EXISTS (SELECT 1 FROM lenses.lenses WHERE id = v_lens_<name>) THEN
INSERT INTO lenses.lenses (id, lenser_id, visibility, status)
VALUES (v_lens_<name>, v_author, 'public', 'published');
INSERT INTO lenses.versions (id, lens_id, version_number, template_body, status, published_at)
VALUES (v_ver_<name>, v_lens_<name>, 1, '<prompt body>', 'published', now());
UPDATE lenses.lenses SET head_version_id = v_ver_<name> WHERE id = v_lens_<name>;
INSERT INTO lenses.version_parameters (id, version_id, label, tool_id) VALUES
(v_p_<name>_a, v_ver_<name>, '<param_a>', v_tool_textarea);
INSERT INTO content.entity_translations (entity_type, entity_id, language_code, is_original, title, description, content)
VALUES ('lens', v_lens_<name>, 'en', true, '<title>', '<description>', '<short content>');
INSERT INTO content.tag_map (entity_type, entity_id, tag_id) VALUES
('lens', v_lens_<name>, v_tag_<slug>),
('lens', v_lens_<name>, v_tag_template)
ON CONFLICT DO NOTHING;
END IF;Resolve v_author from the reserved handle ('lenserfight', 'chainabit', or 'conectlens') at the top of the DO block.
Step 4 — Rays
If your template introduces a ray that does not yet exist, add it to supabase/migrations/20270812000000_canonical_production_tags.sql and create a matching .lenserfight/rays/<slug>/RAY.MD. Never invent rays in template SQL — always go through the canonical migration.
Step 5 — Disclaimers (legal / finance only)
If your template touches legal-adjacent or financial content:
- The prompt body MUST instruct the model to include the disclaimer verbatim in every output.
- The
disclaimer:field in the frontmatter is mandatory. - A pgTAP test fails if the disclaimer marker string is missing from the
template_body.
Legal disclaimer (verbatim):
This review is an analysis aid only and is NOT legal advice. It does not establish an attorney-client relationship. Always have a qualified, licensed lawyer in your jurisdiction review the actual document before signing, negotiating, or relying on it.
Finance disclaimer (verbatim):
This explanation is an analysis aid only and is NOT certified financial advice, audit work, investment recommendation, or tax guidance. Verify all figures against source records and consult a qualified professional before acting on this material.
Step 6 — Validate
Run locally:
pnpm supabase:combine-seeds # rebuild seed.sql
pnpm supabase:reset # drop + reseed local DB
pnpm nx test web --testPathPattern=lens-page # if you have a page testThen visit the lens page in the running web app and confirm it opens, parameters render, and the disclaimer (if applicable) appears in test runs.
Step 7 — Tests
The seed-quality test suite (supabase/tests/31_seed_quality.sql) checks:
- Every public lens has a non-empty
template_body. - Every public lens has at least one ray.
- Every legal-/finance-rayed lens contains the disclaimer marker.
- Every workflow node references an existing lens version.
- Every battle references an existing lens or workflow.
- No template author is alice/bob/carol (legacy demo users).
You do not need to add a new test for each template — the existing tests cover the invariants. If your template introduces a new invariant (e.g. a new disclaimer class), extend the test file in the same PR.
Step 8 — Open the PR
Title format:
seed(<category>): add <slug> templateBody checklist:
- [ ] LENS.MD / WORKFLOW.MD / BATTLE.MD / AGENT.MD added under .lenserfight/
- [ ] SQL seed block added to the matching 4*_*templates.sql file
- [ ] Rays exist in 20270812000000_canonical_production_tags.sql
- [ ] Disclaimer present (if legal / finance)
- [ ] pnpm supabase:reset succeeds locally
- [ ] Template page loads in devCommon pitfalls
- Forgetting the manifest. Add new seed files to
supabase/seed.manifestor they won't be picked up bypnpm supabase:combine-seeds. - Putting tokens in the prompt. Never inline a real API key in
template_body— use[[:param]]and surface the secret via~/.lenserfight/.env.tokens. - Reusing a slug across categories. Slugs are globally unique. If
pr-triage-briefalready exists, do not create another. - Skipping the disclaimer. Tests fail and the PR is blocked.
- UUID drift. The
seed_uuidin frontmatter MUST match the SQL constant exactly. Runrg <uuid>to confirm.