templates.ts · CHAT_TEMPLATES[]: id, name, portrait, description, routines (name, instructions, cron)Atlas recap
2026-10-19
A Fimo agent is six blocks: template, Studio skill, shared crawl, turn framing, data tools and a gallery copy. The template is copied into the database once, while the skill runs live. That split is the main maintenance risk.
How the agent templates, Studio skills, crawl, turn, data tools and gallery fit together, and where the shape leaks.
Outcome
A Fimo agent such as SEO, GEO or Content is not one object in the code. It is assembled from six blocks. A template in packages/agents holds the identity and the routines. A Studio skill holds the method. The shared site crawl holds the deterministic checks. The turn framing builds the prompt. The data tools supply outside numbers. A dev-only gallery catalog describes all of it again for people. PR 3170 does not change this shape. It adds content to it: new routines, new skill topics, DataForSEO operations and crawl fixes.
The blocks connect at two different times. When a project opens an agent, the API copies the template into the database: one agent_threads row for the agent, and one agent_thread_routines row per routine, with the instructions stored verbatim. When a routine runs, the turn reads that copied row, the model loads the skill, which is live code from the current API build, and the skill writes the crawl script into the sandbox.
The main problem follows from that split. The template is copied once and never reconciled, but the skill always runs at the latest version. A Fimo improvement to a skill reaches every project on the next deploy. A new routine or a better instruction reaches only projects that open the agent for the first time. The new routines in this PR (AI visibility, Competitors, Keyword research, Writing) therefore do not reach existing SEO, GEO or Content agents.
The map
Read the map from top to bottom. The top layer says who the agent is and when it works. The middle layer says how it works. The bottom layer is what the API owns at runtime: the database copy, the turn, and the tools.
The gallery is the odd one out. It is a separate, hand-written description of the same agents for the dev-only /components/agents page. It does not feed the product.
Who + when
templates.ts · CHAT_TEMPLATES[]: id, name, portrait, description, routines (name, instructions, cron)How
skills/<agent>/skill.ts: main doc, references{topic}, files{site.mjs}skills/site-crawl/: crawl script, SITE_RULES, SITE_DATAstudio/turn.ts: <agent> block, routine reminder, report ruleOutside data
dataSearch / dataDescribe / dataCall over a generated catalog (729 operations)Gallery
pages/components/agents/catalog.ts: every agent and routine described againBlock 1 · Template
All five agents live in one array, CHAT_TEMPLATES, in templates.ts. A template has no logic. The description is the site stance: what the agent watches and what it never does without a person. Each routine is one sentence of task plus a cron. The routine never explains the method; it names the skill to load.
createdWithProject splits the agents in two groups. SEO and GEO start with every new project, routines on. Content, Quality and Compliance open only from the Chats “+” menu, so a project pays for no run it did not ask for.
CHAT_TEMPLATES[0]the SEO template as JSON · 2 of 9 routines shown{
"id": "seo",
"name": "SEO",
"summary": "Titles, descriptions, broken links.",
"description": "Keep every page of this site findable on Google: …",
"portrait": { "shape": "pebble", "eyes": "ring", "mouth": "smile", "color": "blue" },
"createdWithProject": true,
"routines": [
{
"name": "Technical SEO",
"instructions": "Load the fimo-seo skill and check the technical basics of every page: …",
"schedule": { "cron": "0 4 * * *" }
},
{
"name": "Keyword research",
"instructions": "Load the fimo-seo skill and find the searches worth winning next …",
"schedule": { "cron": "0 10 2 * *" }
}
]
}
model per routine (FIMO-2833) is absent here, so the routine default applies.API contracts
Studio reads the template list to fill the “+” menu and the empty-agent starter prompts. To open an agent, Studio posts a chat create with only template. The DTO forbids kind, title and description beside it: the template brings them. Both calls return nothing when the organization does not have the agents feature.
After that, every routine call (list, edit, pause, run now) works on the database rows. No API call reads the template again for an existing agent.
/chat/project/threads/templates?projectId=…
The default agents for the “+” menu
Returns CHAT_TEMPLATES as-is, parsed through ChatTemplateListDTOSchema. The same list for every project. Empty when the agents feature is off.
200 · ChatTemplateListDTOresponse{
"templates": [
{
"id": "seo", "name": "SEO", "summary": "Titles, descriptions, broken links.",
"description": "Keep every page of this site findable on Google: …",
"portrait": { "shape": "pebble", "eyes": "ring", "mouth": "smile", "color": "blue" },
"routines": [{ "name": "Technical SEO", "instructions": "…", "schedule": { "cron": "0 4 * * *" } }]
}
]
}
createdWithProject and the routine model: zod drops unknown keys on parse./chat/project/threads
Opens the project's one chat for a template
201 when this call created the agent, 200 when it already existed (the archived chat is restored for the opener). 404 Unknown agent template for an unknown id.
ProjectChatCreaterequest body{
"projectId": "…",
"template": "seo",
"timezone": "Europe/Paris"
}
.refine: when template is set, kind, title and description must be absent.timezone is the opener's, because projects have no timezone. createDefaultAgents passes none, so a new project runs in UTC./chat/project/threads/:threadId/routines[/:routineId[/run|/runs]]
Work on the copied routine rows
Mounted under projectChatsRouter. They read and write agent_thread_routines only. None of them know a routine came from a template.
Instantiation
openTemplateChat runs one transaction. It inserts the agent chat with the template's title, description and portrait, and records template = 'seo'. A unique index on (project_id, template) keeps one agent per template per project. Only when the insert really created the row does it insert the routines, each with its own run thread, and the empty memory files.
The routine row stores instructions “verbatim, each time the routine runs”. The row has no link back to the template routine it came from: no template key and no revision.
apps/api/src/domain/agents/chat/threads.tsopenTemplateChat, as pseudocode1template = findChatTemplate(templateId) // or 'unknown_template'
2transaction {
31 chat = insert agent_threads { kind:'agent', title, description, portrait, template: id }
4 onConflictDoNothing(project_id, template)
52 if (chat was created) {
6 for routine in template.routines:
7 runThread = insertRoutineThread(parent: chat)
83 insert agent_thread_routines templateRoutineValues(routine) // copy
9 ensureAgentMemoryFiles(MEMORY.md, NOTES.md per routine)
10 }
11}
124if (!created) return existing chat, un-archived // no routine sync
One agent per template. The partial unique index makes two simultaneous clicks produce one chat.
Routines only on first creation. No chat with half its routines is ever visible.
The copy. Name, instructions, model and a project-offset cron are written into the row. The template is not read again.
The frozen branch. An existing agent returns as it is. Routines added to the template later are not added here.
'agent''seo' · unique per projectRuntime
The minute sweep claims a due routine and starts a turn in its run thread. The turn builds a stable system prompt. The <agent> block holds the name, the description and the routine ids. The routine message holds the copied instructions. A reminder adds only what changes, such as a failing streak.
The model then calls skillLoad fimo-seo. This reads the skill from the running API build, so it is always current. Reading a topic reference with the session id writes site.mjs into the sandbox. The crawl and the topic check are code. The model ranks, fixes, and reports at most once.
flowchart TB Sweep[Minute sweep] -->|claims row| Row[Routine row<br/>copied text] Row --> Turn[Turn<br/>agent block] Turn --> Model[Model] Model -->|skillLoad| Skill[fimo-seo skill<br/>live build] Skill -->|writes site.mjs| Box[Sandbox<br/>crawl + check] Model -->|dataCall| Data[Data tools] Model -->|only if needed| Report[Report<br/>to agent chat]
buildAgentPrompt()turn.ts · what the model sees<agent name="SEO" memory="/memory/agents/…/MEMORY.md">
You are Fimo, acting as this site's agent named SEO. The team describes its job as:
Keep every page of this site findable on Google: …
Its routines:
- 3f2a…: Technical SEO · notes: /memory/…/NOTES.md
- 91bc…: Broken links · notes: …
How to maintain a website lives in skills, when present; the description says what is specific to this site.
Its memory is …: its chat and every run of its routines share it …
</agent>
buildAgentPrompt.Blocks 2 + 3 · Skill and crawl
A Studio skill is a VirtualSkill object, built into the API like the chat-UI skill, so it works on every project whatever its CLI version. skills/index.ts registers all six in one source called fimo-studio. The main document holds the stance-free method. Each routine topic is a separate reference, read only when needed.
site-crawl/ is the shared home. siteCrawlSteps(skill, topics) writes the same crawl instructions into four skills. SITE_RULES says what a run may change alone. SITE_DATA (new in this PR) says when to use Search Console and when to pay for data. The script itself does the crawl and the checks deterministically.
fimoSeoSkill: VirtualSkillshape as JSON · content shortened{
"name": "fimo-seo",
"source": "fimo-studio",
"description": "Load whenever the work touches SEO or how the site ranks on Google: …",
"content": "# Fimo SEO\n${siteCrawlSteps('fimo-seo', '`technical`, `meta-tags`, …')}\n## Give a change time …\n${SITE_DATA}\n## Rank the work by real searches …\n${SITE_RULES}",
"references": {
"technical": "# Technical SEO\n`check --topic technical`: status, redirects, …",
"meta-tags": "…", "broken-links": "…", "image-seo": "…",
"internal-linking": "…", "hreflang": "…", "monitoring": "…",
"keywords": "…", "keyword-research": "…"
},
"files": { "site.mjs": "<SITE_SCRIPT>" }
}
flowchart TB Rules[site-crawl<br/>rules + script] Rules --> SEO[fimo-seo] Rules --> GEO[fimo-geo] Rules --> Content[fimo-content] Rules --> Quality[fimo-quality] Rules -.-> Compliance[fimo-compliance]
--compliance mode. This is the healthiest part of the graph.Block 5 · Outside data
The model sees three tools: dataSearch to browse groups, dataDescribe to read one operation, and dataCall to run it. The catalog is generated by generate-data-catalog.mjs and lists only reviewed operations. This PR adds 24 DataForSEO operations, blocks two broken providers, and keeps the flat per-call fee in prices.
The data layer knows nothing about SEO. The skill text (SITE_DATA and each topic reference) names which operation a topic uses. That is the right direction: domain knowledge stays in the skill, transport stays in the catalog.
DataOperationone catalog entry · packages/ai/src/agents/data/types.ts{
"id": "ahrefs/site-explorer/paid-pages",
"group": "ads",
"provider": "ahrefs",
"endpoint": "/site-explorer/paid-pages",
"description": "List a target's paid-search landing pages with traffic and ad counts.",
"price": { "usd": 0.008385, "per": "result" }
}
price.flatUsd, the fee charged once per call.adapters/apps.ts.Block 6 · Gallery
catalog.ts feeds the dev-only /components/agents page. It lists shipped and proposed routines with fields the template does not have: triggers, what a routine fixes alone, what it asks first, cost, outside data and price chips. A test keeps one thing aligned: every template routine is listed as Available, and nothing else is.
Everything else is free text that can drift from the template and the skill. This PR changed 140 lines here to follow the template and skill changes.
CatalogRoutineone gallery entry, shape as JSON{
"id": "…", "agent": "seo", "name": "Technical SEO",
"template": "Technical SEO", // matched by NAME to CHAT_TEMPLATES
"availability": "yes",
"does": "…", "fixes": "…", "asks": "…",
"when": { "weekly": "…" },
"reads": [{ "source": "Search Console", "cost": "free", "why": "…" }]
}
Problem A · Leaking
The copy is the only path from template to database, and it runs only when the agent chat is first created. No reconciler, migration, or template revision exists. The one manual way out is the staff ops command, where a person types each missing routine into a spec, project by project. Every later template edit therefore splits projects in two populations, and a new deploy cannot fix the old one.
The skill makes this worse, not better. It moves forward on every deploy, so an old routine instruction can point at a skill topic that has changed or does not exist yet. This PR shows the problem: its four new routines appear only for projects that open SEO, GEO or Content after the deploy. SEO and GEO are created with every project, so most existing projects already have them and never receive the new routines.
flowchart LR T1[templates.ts<br/>v1] -->|copy on open| P1[Project A<br/>4 GEO routines] T2[templates.ts<br/>v2 + AI visibility] -->|copy on open| P2[Project B<br/>5 GEO routines] T2 -. no path .-> P1 K[fimo-geo skill<br/>always latest] --> P1 K --> P2
Problem B · Leaking
The routine “Technical SEO” says “check the technical basics”. The skill has a reference keyed technical, and its crawl steps list the topics as one string. The model must map one to the other. Nothing typed connects them, so a renamed topic or a new routine with no reference compiles and passes tests.
A comment in templates.ts states the rule “one routine per topic of the skill”. The code does not enforce it.
routine → topictoday vs a typed keyToday
1{ name: 'Technical SEO',
2 instructions: 'Load the fimo-seo skill and check the technical basics …' }
3// skill: references.technical + "`technical`, `meta-tags`, …"
Proposed
1{ key: 'technical', name: 'Technical SEO',
2 topic: seoSkill.topics.technical } // type-checked
3// instructions generated: "Load fimo-seo, topic `technical`."
Problems C, D, E
To add one SEO routine today you edit the template array, the SEO skill reference, the topic string in the crawl steps, and the gallery catalog. The E2E template list may also need an edit. None of these edits is wrong alone. Together they make the agent hard to see as one unit.
Acceptable while the page is dev-only. Removable when the routine metadata (reads, fixes, asks) lives beside the routine definition and the page derives from it.
templates.ts holds all five agents in one 250-line file. The skill is in studio/skills/seo/. Shared crawl is in site-crawl/. The width is necessary; the scatter is not.
Skills are outdent template literals with interpolation. They are hard to read as documents. A second skill home exists in apps/cli/assets/skills with different shipping rules.
Target shape
Co-locate each agent: identity, routines and skill in one folder. Give each routine a stable key and a typed topic. Then let the database row point to the template routine instead of copying its text. The turn reads the instructions from code unless a person overrode them. Fimo improvements then reach every project on deploy, like skills already do. User edits are kept because an override is explicit.
This is a proposal, not code in the PR. The folder below does not exist yet.
packages/agents/src/fimo/agents/studio/agents/proposed layoutagents/
seo/
agent.ts // defineAgent({...}) — identity + routines
skill.md // main doc (markdown, not an outdent string)
topics/
technical.md meta-tags.md broken-links.md …
geo/ content/ quality/ compliance/
shared/
site-crawl/ script.ts rules.md data.md
index.ts // CHAT_TEMPLATES = [seo, geo, …]; studioSkillSources derived
agents/seo/agent.tspseudocodeexport default defineAgent({
id: 'seo', name: 'SEO', portrait, createdWithProject: true,
description: 'Keep every page of this site findable on Google: …',
skill: { name: 'fimo-seo', doc: './skill.md', topics: ['technical', 'meta-tags', …], shared: [siteCrawl] },
routines: [
{ key: 'technical', name: 'Technical SEO', topic: 'technical', cron: '0 4 * * *',
gallery: { reads: [searchConsole], fixes: '…', asks: '…' } },
…
],
})
// defineAgent type-checks: every routine.topic ∈ skill.topics; keys unique.
'seo/technical' · null for a hand-made routineflowchart TB Def[defineAgent<br/>agents/seo] --> Tpl[CHAT_TEMPLATES] Def --> Skill[fimo-seo skill] Def --> Gallery[Gallery page] Tpl -->|template_key only| Row[Routine row] Row -->|instructions null?| Live[Read live<br/>from template] Open[Open agent or deploy] -->|add missing keys| Row
template_key the agent lacks. A person's deleted routine is remembered and not re-added.Pruning and evidence
After the target lands, these become deletable: the comment-only “one routine per topic” rule, the topic list passed as a string to siteCrawlSteps, most of catalog.ts (only proposal-only rows remain), and the copy-semantics comments in templates.ts and openTemplateChat. The verbatim copy in templateRoutineValues shrinks to a key.
This recap is source inspection of the PR head. It does not measure how many existing projects already miss the new routines.
Read at 0fa7c9e: template, DTOs, routes, openTemplateChat, routine schema, turn framing, SEO skill, crawl rules, data types, gallery catalog.
openTemplateChat inserts routines only inside if (created …). The staff ops path (createOpsAgentChat, FIMO-2817) adds template routines only when it creates the agent. For an existing agent, it adds only routines a staff member writes in the spec, matched by name.
Not queried. Next check: count agents with template = 'geo' that have no routine named “AI visibility”.
Not run by this recap. The PR body reports focused unit and live data tests.