Atlas recap

2026-10-19

Fimo agent templates: the parts and how they connect

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.

Scope PR 3170 head, architecture only Base feat/fimo-apps-proto · 92eabcf Depth Normal Evidence Source inspection Source read

Source read

PR 3170 · Agent templates architecture

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

Six blocks make one agent, in three packages

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

1 · Templatetemplates.ts · CHAT_TEMPLATES[]: id, name, portrait, description, routines (name, instructions, cron)

How

2 · Studio skillskills/<agent>/skill.ts: main doc, references{topic}, files{site.mjs}
3 · Shared site crawlskills/site-crawl/: crawl script, SITE_RULES, SITE_DATA
4 · Turn framingstudio/turn.ts: <agent> block, routine reminder, report rule

Outside data

5 · Data toolsdataSearch / dataDescribe / dataCall over a generated catalog (729 operations)
5b · AppsSearch Console and Analytics app tools, via the chat apps adapter

Gallery

6 · Gallery catalogpages/components/agents/catalog.ts: every agent and routine described again
Where each block lives12 files
⌄▱packages/agents/src/fimo
⌄▱agents/studio
⌄▱packages/ai/src/agents/data
⌄▱apps/api/src
⌄▱apps/ui/src/pages/components/agents

Block 1 · Template

A template is a static TypeScript record: identity, stance and routines

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 * *" }
    }
  ]
}
  • Optional model per routine (FIMO-2833) is absent here, so the routine default applies.
  • Daily routines are 20 minutes apart. At copy time the API also shifts each cron by a per-project offset.
The five templatestemplates.ts
seo
9 routines · created with project
geo
4 routines · created with project
content
5 routines · “+” menu only
quality
2 routines · “+” menu only
compliance
3 routines · “+” menu only

API contracts

Two HTTP calls expose templates; the routine routes then work on the copies

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.

› GET /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.

NewChatMenu / StarterPrompts useChatTemplates projectChatsRouter CHAT_TEMPLATES
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 * * *" } }]
    }
  ]
}
  • The DTO strips createdWithProject and the routine model: zod drops unknown keys on parse.
› POST /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.

useOpenChatTemplate projectChatsRouter POST openTemplateChat()
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.
› GET · POST · PATCH · DELETE /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.

Contract files2 files

Instantiation

Opening an agent copies the template into two tables, once

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 pseudocode
1template = 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
  1. 1

    One agent per template. The partial unique index makes two simultaneous clicks produce one chat.

  2. 2

    Routines only on first creation. No chat with half its routines is ever visible.

  3. 3

    The copy. Name, instructions, model and a project-offset cron are written into the row. The template is not read again.

  4. 4

    The frozen branch. An existing agent returns as it is. Routines added to the template later are not added here.

agent_threadsagent chat
kind
'agent'
title, description, portrait
copied · the team may edit them
template
text · 'seo' · unique per project
agent_thread_routinesone per routine
thread_id
the agent chat
run_thread_id
the routine's own thread · unique
name, instructions
copied verbatim
schedule_cron, schedule_tz
shifted cron · opener tz or UTC
model
null = default
paused_at, next_run_at
sweep state
event_trigger
jsonb · FIMO-2714
(no template key)
nothing links back to the template
Copy and storage3 files

Runtime

A routine run mixes the frozen copy with the live skill

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]
The sweep, row and turn use the copy made when the agent was opened. The skill, crawl script and data catalog come from the current API build.
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>
  • Ids and memory paths are illustrative. Field order and wording come from buildAgentPrompt.

Blocks 2 + 3 · Skill and crawl

The skill is the deep module: one doc, topic references, and a shared script

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]
One shared crawl serves four skills, and compliance uses its --compliance mode. This is the healthiest part of the graph.

Block 5 · Outside data

Data tools are a generic catalog; skills decide when to pay for them

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" }
}
  • DataForSEO entries add price.flatUsd, the fee charged once per call.
  • Search Console is not in this catalog. It is an app tool, wired for routine and Slack runs in adapters/apps.ts.

Problem A · Leaking

Template changes never reach agents that already exist

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
The template splits projects by opening date, while the skill moves every project forward together.

Problem B · Leaking

Only prose links a routine to its skill topic

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 key

Today

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

One agent change touches three or four places

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.

TransitionalC · Gallery catalog duplicate

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.

Poor localityD · One agent in three folders

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.

MildE · Prose in TypeScript strings

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

One folder per agent, typed routine keys, and instructions read live

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 layout
agents/
  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.tspseudocode
export 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.
agent_thread_routinesProposed
template_key
text · 'seo/technical' · null for a hand-made routine
instructions
nullable · null = read live from the template
schedule_cron
unchanged · a person's edit stays theirs
flowchart 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
One definition feeds the template, the skill and the gallery. The routine row keeps only a key and a person's overrides.
  • Fimo owns glue, people own choicesInstructions stay Fimo's until a person edits them. Name, cron, pause and model stay with the row.
  • Missing routines are added, never removedA reconcile on open or deploy inserts routines whose template_key the agent lacks. A person's deleted routine is remembered and not re-added.
  • Every routine names a real topicChecked by the type system and one test, not by a comment.

Pruning and evidence

What becomes deletable, and what this recap actually checked

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.

VerifiedSource inspection

Read at 0fa7c9e: template, DTOs, routes, openTemplateChat, routine schema, turn framing, SEO skill, crawl rules, data types, gallery catalog.

VerifiedFrozen copy

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.

UnknownImpact on staging and production

Not queried. Next check: count agents with template = 'geo' that have no routine named “AI visibility”.

UnknownPR tests and CI

Not run by this recap. The PR body reports focused unit and live data tests.

Source peek

Open at head
Path copied