Catalog¶
The catalog is Factoryal's context lake: what exists, who owns it and what depends on what. Agents read it, workflows write to it, and every change can start a run. Its model follows Port.io's (blueprints, entities, relations, search), and the API shapes are Port-compatible.
erDiagram
team ||--o{ service : owns
service ||--o{ repo : "built from"
service ||--o{ issue : "affected by"
repo ||--o{ issue : "fixed in"
Blueprints¶
A blueprint is a type: a JSON schema for its properties, plus named relations to other blueprints.
kind: Blueprint
identifier: service
title: Service
schema:
properties:
tier: { type: number, description: "1 = business critical" }
language: { type: string }
lifecycle: { type: string, enum: [experimental, production, deprecated] }
required: [lifecycle]
relations:
owner: { target: team, required: true } # many: true for a list
Property types are string, number, boolean, object and array. Other JSON-schema
keywords (enum, format, minimum, items, ...) pass through and are enforced. The full
sample catalog is examples/demo/catalog/catalog.yaml.
| Action | API |
|---|---|
| Create / replace / delete | catalog/*.yaml in a factory repo (kind: Blueprint) |
| Read / list | GET /v1/blueprints/<id>, GET /v1/blueprints |
The catalog holds two kinds of thing, told apart by where they come from:
- Factory config: agents, skills, rules, runners and webhook sources. They're defined in a
factory repo and are not part of the catalog: they live in their own tables
(the Postgres schema
config) and have their own read endpoints (Agents). Before that they were entities of the blueprints_agent,_skill,_rule,_environmentand_webhook, which no longer exist. - Data: everything else, written through the API, by workflows and by integrations. This
includes agents' memory lessons (
_memory).
If it's in your factory repo, it's config; everything else is data. Blueprints whose names
start with _ are platform-managed: admin-only, and hidden from the Catalog page unless you
ask for them. Integrations create the blueprints they need on first use (slack_message,
linear_issue).
Entities¶
kind: Entity
blueprint: repo
identifier: billing-api
title: Billing API
team: payments # one or a list; drives ownership, budgets and $team rules
properties:
url: https://github.com/acme/billing-api
default_branch: main
has_readme: true
relations: { service: billing }
| Action | API |
|---|---|
| Create | POST /v1/blueprints/<bp>/entities |
| Replace / merge | PUT / PATCH /v1/blueprints/<bp>/entities/<id> |
| Read / list / delete | GET, DELETE /v1/blueprints/<bp>/entities/<id>, GET /v1/blueprints/<bp>/entities |
Every write is validated against the blueprint. It is committed in the same transaction as
an outbox event (entity.created, entity.updated, entity.deleted), so entity_event
triggers see every change exactly once (Architecture).
Search¶
POST /v1/entities/search (or /v1/blueprints/<bp>/entities/search) takes Port's query
shape. Agents use the same language through their search_catalog tool.
{
"combinator": "and",
"rules": [
{ "property": "$blueprint", "operator": "=", "value": "service" },
{ "property": "tier", "operator": "<=", "value": 1 },
{ "combinator": "or", "rules": [
{ "property": "language", "operator": "=", "value": "python" },
{ "property": "$team", "operator": "containsAny", "value": ["payments"] }
]}
]
}
| Rule | Form |
|---|---|
| Property | {property, operator, value}. property is a property name or $identifier, $title, $blueprint, $team, $createdAt, $updatedAt |
| Operators | =, !=, >, >=, <, <=, in, notIn, contains, doesNotContains, containsAny, beginsWith, endsWith, isEmpty, isNotEmpty, between |
| Graph | {operator: relatedTo, blueprint, value, direction}: entities connected to the given ones, transitively (up to 10 hops) |
| Groups | {combinator: and \| or, rules: [...]}, nested freely |
For relatedTo, direction: upstream follows relations outward (what the given entities
depend on) and downstream follows them inward (what depends on them). Without a direction
it goes both ways. For example, every issue and repo that hangs off the billing service:
{ "rules": [{ "operator": "relatedTo", "blueprint": "service", "value": "billing", "direction": "downstream" }] }
The factory scorecard¶
Every applied factory is graded on which Factoryal features it configures, so it's clear what it isn't using yet. About 45 checks in eight areas:
| Area | What it checks |
|---|---|
| Agents | agents exist, pin a model, hold no unrestricted repo:write / catalog:write |
| Skills and rules | agents load skills, no skill sits unused, skill variants are compared in a benchmark, an org rule exists, repo/team rules when code changes |
| Memory and learning | lessons are recorded (by retros, or agents with memory:write on an agent step, which actually did in the last 30 days), steps read them back (memory: or a code step), the loop is closed, every fixed subject read is one an agent may write, agents reading webhook or entity payloads use memory_review: always, the review queue isn't stale; and the learning loop: runs that go wrong get a retro (from run_event), so do PRs reviewers turn down (outcome), an improver with proposals:write turns each new episode of a _cluster into a proposed fix, retro spend is capped, and retros were actually written for recent failures |
| Automation | workflows start from events, every webhook source is used, something runs on a schedule, results are posted back (notify) |
| Guardrails | factory and project budgets, a budget_event workflow, a person approving before code that opens PRs, max_cost_usd, timeout on llm, agent, code and http steps, concurrency on event-driven workflows |
| Coding | code steps set a runner (environment); pull requests get an agent review |
| Quality loop | benchmark suites with tasks for every coding agent, run on a schedule, with a benchmark_event alert |
| Operations (live only) | from the last 30 days: work is running, runs succeed (from 5 finished runs), approvals are answered and mostly accepted (from 3 decided), spend is within budget, every suite ran recently without regressing (only runs launched while this factory owned the suite count; a run of a suite no factory owned, or one an earlier factory owned, isn't counted, so a suite this factory just took over shows as not run in the last 30 days until its next run) |
A person approves before code is written when the workflow has an approval or wait step, or
when every automatic trigger is a webhook or entity_event whose condition checks a status,
label or approval a person sets (an issue moved to Ready for Dev).
Each check is pass, partial, missing or n/a (it doesn't apply, e.g. runner checks
without code steps), weighs by impact (high 3, medium 2, low 1), and lists the resources it
found. The score is the weighted share passed (partial counts half): Basic below 40, Bronze
from 40, Silver from 65, Gold from 85. Two levels also have a gate:
- Silver needs a closed learning loop: lessons recorded (by agents or retros) that steps read back. A factory that doesn't learn stays at Bronze.
- Gold needs every high-impact check passing.
When a gate holds a factory back, capped says which level the score reached and what is
missing. Failing checks come back as recommendations, highest impact first, each saying what to
add to the factory repo. The Learning inbox ranks them with everything
else waiting on a person.
Projects. Each project is graded on its own: what it owns plus the shared resources at the
factory's root (agents, skills, rules, runners, webhooks, catalog, benchmarks), and the shared
workflows its steps start. Its budget check looks at its own project: budget, and its live
checks at its own runs.
Trend. Every apply saves the factory's score and level (from its configuration alone) with
the version, so trend shows how each commit moved it, oldest first.
GET /v1/factories/<name>/scorecard(admins only: it shows agents' scopes, costs and failures) grades the live factory, with runtime signals such as lessons waiting for review, one grade per project underprojects, andtrend;GET /v1/factory-scorecardsgrades every factory (an empty list for anyone but admins). Each is cached for 30 seconds. The web UI shows them at the top of Scorecards, with a tab per project and the trend next to the score.factoryal factory check <dir>prints the same scorecard for a folder, before you push it.
Where catalog data comes from¶
| Source | Writes |
|---|---|
| Factory repos | blueprints and seed entities (catalog/) |
| The API | entities of your own blueprints |
| Workflows | upsert_entity steps, and agent steps with catalog:write[:<bp>] |
| GitHub App | repo facts (has_readme, has_ci, main_ci_status, ...) on pushes to the default branch and on CI results (GitHub App) |
| Slack / Linear | slack_message and linear_issue entities (Slack and Linear) |
| Webhook sources | one entity per delivery, through entity: mapping (Factories) |