Feeds
A feed is the root record in the catalog. Files, notes, addresses, tags, and content types are all
feeds, stored in one table and distinguished by a type column. A feed’s
references record where its bytes live, and its
analyses record what xixo found in it.
The type comes from a small, fixed list. The browser app renders each feed according to its type.
| Type | What it is | Singleton |
|---|---|---|
xixo:file |
Bytes stored in a resource, or staged until a resource accepts them. | No |
xixo:note |
Text written directly into the catalog. | No |
xixo:address |
An address, such as /buy, that can run on a schedule. |
Yes |
xixo:tag |
A label that a person, an agent, or an analysis filed something under. | Yes |
xixo:mime |
A content type, keyed by its name, such as text/markdown. |
Yes |
Every feed has a key of at most 900 characters. A file’s key is its filename and a note’s key is
its title. These keys do not have to be unique, so two files named invoice.pdf are two feeds.
A singleton is unique on (tenant, type, key). The model and a partial unique index both enforce
this, so a tenant has one /buy, one tag named receipts, and one text/markdown. Feed.tag! and
Feed.mime! find the row or create it. Tags and addresses are looked up by key, and Feed.tag!
ignores case and reads underscores as spaces, so Acme and ACME are one tag, and so are
Mortgage_Statement and mortgage statement.
An address key is a slash followed by up to 63 lowercase letters, digits, and dashes, starting with
a letter or a digit. Paths that xixo already serves, such as /graphql, /mcp, /uploads, and
/feeds, are refused.
Content types are feeds of their own. This keeps the tag list limited to labels that someone chose, without the types that a parser detected.
Origin
Section titled “Origin”origin is resource for a feed that came from syncing a resource, which is the default, and
feed for a feed that an analysis created. A synced feed cannot be changed to feed later.
Lifetime
Section titled “Lifetime”A feed lasts forever unless it has an expires_at. Every hour, ForgetExpiredJob forgets the
feeds whose date has passed. Forgetting removes the feed’s references, analyses, and edges, and
deletes any bytes xixo created for it, the same as forgetting it by hand. This suits things that go
stale, such as a forecast, a price, or an API response.
When an agent answers a question, anything it creates or keeps (a note, a page snapshot, or a kept
object) lasts 30 days. The agent can pass lasts as forever or as a number of days to change
that. Keeping something that already existed leaves its lifetime unchanged. Outside a question,
anything a tool creates lasts forever unless lasts is given.
An agent changes the lifetime of something it created with the feed tool and do=last. A person
can change the lifetime of any feed with the setFeedLifetime mutation, or with Keep forever on
the item’s page.
Parents and children
Section titled “Parents and children”Some files contain other files, such as an email with attachments. When an analyzer can extract
them, each becomes a xixo:file feed whose parent is the file it came from. Its bytes are written
to the internal children store, keyed by the parent’s id, a position, and the filename. Each child
is analyzed in turn.
Extraction stops four levels below a top-level feed. Destroying a feed destroys its children.
An edge connects two feeds. Edges have no direction and no label. Each pair is stored once with
a_id < b_id: the model sorts the pair before writing it, and a check constraint rejects a row that
is not sorted. As a result, a pair cannot be stored twice in reverse, and a feed cannot connect to
itself.
Tags and content types are built from edges. Filing a file under receipts creates an edge between
the file and the xixo:tag feed keyed receipts. Analysis connects every file it reads to the
xixo:mime feed for its content type. The files tagged receipts are the feeds at the other end of
that tag’s edges. Destroying a feed deletes all of its edges.
A tag or content type that nothing is connected to any more is deleted: when the last item filed
under it is deleted, when it is taken off the last item, or when an analysis stops giving it. A tag
someone wrote a note on stays. ForgetLonelyFacetsJob also runs at 4:40 a.m. and deletes any left
empty some other way.
A finished analysis files its feed under a tag for each of its summary’s tags, leaving out any tag
Feed.fit_tag? refuses. A fit tag has at least two letters or digits, no more digits than letters,
and is not shaped like a date, an email address, a domain, or a citation such as feed 12. It is not the feed’s
own key or title, and when the file’s name has a digit in it, as a camera’s GIG_2081.NEF does,
it does not contain that name. So NEMA 14-50R is a tag, and $2,409.33, June 2026, and
GIG 2081 are not. Amounts, dates, account numbers, and addresses stay searchable in the body.
The agent’s connect tool refuses the same tags, and says why. An item’s tags are listed most shared first. Those edges are marked
inferred. The next analysis that finishes replaces the inferred edges with its own, and leaves the
edges a person or an agent made. Tagging a feed by hand with a tag analysis found makes that edge a
person’s, so it stays. A failed analysis keeps the tags the feed had. A feed that takes another’s
place, when the same bytes join, takes its edges with their marks.
Schedules
Section titled “Schedules”An address has no bytes to read, so its work is described by a prompt. Its schedule holds the
prompt, the number of turns the agent may take (six by default), and an interval in seconds. Only a
xixo:address can have a schedule, and it has at most one.
| Field | |
|---|---|
prompt |
What the address should find. Required. |
turns |
The agent’s turn limit for one run. |
interval |
At least 60 seconds, or empty to run only by hand. |
paused_at |
Set to stop the schedule while keeping its interval. |
next_run_at |
Maintained by the model. Cleared while paused or unscheduled. |
Every minute, a recurring job finds the schedules that are due, opens an analysis on each with the
cause schedule, and moves next_run_at forward by one interval.