Skip to content

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 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.

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.

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.

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.