# Create, update, and connect Topics Part of `skills/topics/SKILL.md`, which is the map. This is the detail on every write: the run's invocation session, creating a Topic and recovering its address, what a call's answer does and does not prove, the Topic verbs, references between Topics, and where each part of a Topic goes. ## One invocation session per run Every write below passes `--invocation`, the caller's own id for that one mutation. An id is replayable only within the invocation session it was chosen in, so `cf` refuses an id when no session is in scope (`CF_INVOCATION_SESSION` or `--invocation-session`). The session has to outlive any one shell, because a retry may come from a later tool call. Mint it once, at the start of the run, into a file at a path private to this run: ```bash (umask 077 && set -C && s="$(deno task cf invocation-session new)" && echo "$s" > '') ``` The file is written only once the mint succeeds. `set -C` makes a second mint fail rather than replace the session. A run that minted a fresh session per command would make every retry a new invocation, and a retried write would land twice. Treat the file as a credential: `umask 077` makes it readable only by you. Never print its contents. Every write command below starts by reading the session back into the environment. Replace every angle-bracketed invocation placeholder with an id unique to that logical mutation, and reuse that id only to retry the same mutation. ## Create and recover the address Create through the board and project the returned Topic to its address: ```bash export CF_INVOCATION_SESSION="$(cat '')" CREATE="$(deno task cf piece call --cell "$TOPICS_BOARD" \ --invocation '' \ addTopic \ '{"title":"","body":"<the core issue>","agentName":"Sol"}' \ -- --schema '{"properties":{"topic":{"$link":true},"name":{"type":"string"}}}')" TOPIC="$(printf '%s\n' "$CREATE" | jq -r '.result.topic["$link"] // empty')" NAME="$(printf '%s\n' "$CREATE" | jq -r '.result.name // empty')" ``` The projection names BOTH results, and that is load-bearing: a schema listing only `topic` drops `name` from the envelope, so `NAME` comes back empty and the allocated number is lost. Dropping the projection entirely returns the name and the whole created topic with it — a rendered view included, two orders of magnitude more payload — which is what the projection exists to avoid. When the result is present, carry `TOPIC` into the next command. `NAME` is the member name the board allocated and passed into the Topic — read it here rather than from the Topic's own `shortName`, because the row IS the created Topic, so reading a property off a piece filed a moment ago waits for that piece to materialize. Use JSON encoding or schema-derived flags for multiline Markdown; do not interpolate unescaped content into JSON. Current Estuary calls have a known observation asymmetry. `addTopic` has reported an error after committing and has reported success without committing. A call can also answer nothing at all: `addTopic` and `addLink` have each hung past a ten-minute client timeout and committed, and an `addTopic` has hung the same way and not committed. A timeout therefore settles nothing in either direction, and none of this is particular to `addTopic` — take it as the behavior of every authored-content verb. So treat every call envelope, and every absence of one, as an observation rather than proof of durable state, and read back after every mutation. For `addTopic`, use a distinctive title and compare the narrow board index before and after the call; if the result is uncertain, recover its `$link` there rather than blindly creating another Topic. Retrying under a new id on the strength of a timeout is how one Topic becomes two. ```bash deno task cf cell get "$TOPICS_BOARD" index --step --select @,title deno task cf cell get --cell "$TOPIC" title --input ``` ## Warm the topic you just filed A Topic's scalars divide in two, and a headless filing only writes one half. `title`, `body` and `createdAt` are durable inputs, written by `addTopic`. `lastActivityAt` and `commentCount` are DERIVED, and a derivation materializes only once the piece has RUN. Creating a topic does not run it, so until something does, the board reads those fields as their declared defaults — `lastActivityAt` 0, `commentCount` 0. Opening a topic in the shell runs it, which is why a topic filed through the UI never shows this and one filed here always does. Ordering does not depend on this: `activityOrderOf` falls back to `createdAt`, so a topic that has never run still sorts by when it was filed. Stepping is what makes its derived fields true, not what puts the card in the right place. So finish a filing by stepping the new topic, which is the headless equivalent of opening it: ```bash deno task cf piece step --cell "$TOPIC" deno task cf cell get "$TOPICS_BOARD" index --step --select @,title,lastActivityAt,commentCount ``` `shortName` is left out of that projection because it is not a derived scalar and stepping does not settle it: a topic running this checkout's pattern publishes the number its durable input holds, so it reads the same before and after a step. Select it when the number is what you want; `references/naming.md` says what the deployment carries. `piece step` runs one scheduling step — start, idle, synced, stop. It authors no new content, so it is safe to repeat and safe to run over a topic somebody else filed — but it is not inert: running a topic whose stored state predates a version bump performs that migration, and the migration is a durable write. A bulk filing is worth a pass over every topic it created. Retry an uncertain mutation only with the same session and id. The full retry and receipt model is in `skills/cf/SKILL.md` and `docs/common/verbs/over-the-cli.md`. ## Update through Topic verbs ```bash export CF_INVOCATION_SESSION="$(cat '<run session file>')" deno task cf piece call --cell "$TOPIC" --invocation '<unique-set-title-id>' setTitle \ '{"title":"<complete new title>","agentName":"Sol"}' deno task cf piece call --cell "$TOPIC" --invocation '<unique-set-body-id>' setBody \ '{"body":"<complete revised body>","agentName":"Sol"}' deno task cf piece call --cell "$TOPIC" --invocation '<unique-add-comment-id>' addComment \ '{"body":"<point-in-time update>","agentName":"Sol"}' deno task cf piece call --cell "$TOPIC" --invocation '<unique-add-link-id>' addLink \ '{"url":"<PR URL>","kind":"pr","label":"<label>","agentName":"Sol"}' ``` `kind` defaults to `web`; a blank or omitted `label` defaults to the URL. Current authored-content verbs reject blank required content or attribution instead of reporting apparent success. Verify the relevant durable input after each call (`title`, `body`, `comments`, or `links`). Use `--step` as a second check when the expected change is computed, such as a count or board-index row. A cross-Topic connection is a reference, not an address pasted into prose. Pass the canonical reference in the declared reference position; the CLI turns it into the live piece link the verb expects. Set `OTHER_TOPIC` to the `$link` from the index row for the Topic being referenced; the row's `{"$link": …}` object passes in that position as it was printed, too: ```bash export CF_INVOCATION_SESSION="$(cat '<run session file>')" export OTHER_TOPIC='<canonical /of:... address from another index row>' deno task cf piece call --cell "$TOPIC" --invocation '<unique-mention-id>' mention \ "{\"topic\":\"$OTHER_TOPIC\"}" deno task cf piece call --cell "$TOPIC" --invocation '<unique-unmention-id>' unmention \ "{\"topic\":\"$OTHER_TOPIC\"}" ``` Use inline JSON for these reference events. The schema-derived `--topic` flag parses its declared object before reference resolution and therefore rejects a bare canonical address. `unmention` removes every `mention`-made edge to that Topic. References created inside the body are removed by editing the body. An `addLink` URL that resolves to a piece also contributes to the reference graph. ## Where each part of a Topic goes A Topic has three places to write, shown in the UI as Body, Thread, and Links. What follows is recommended practice, not a requirement: the pattern accepts any content in any of them, and nothing breaks if a write lands elsewhere. Keeping each to its own job means a reader knows where to look, and later tooling can build on the distinction. - **Body** (`addTopic`'s `body`, then `setBody`): the core issue. What the Topic is about, why it matters, and the current state of understanding and decisions, written so that someone arriving for the first time can read the body alone and know where things stand. Replace it whole when the picture changes, preserving meaningful context and decisions. When an update is critical for that first-time reader (a decision reversed, the work blocked or superseded), put a short note about it at the top of the body as well as in the thread. Fabric owns revision history, so the body does not need to record how it got to its current state. - **Thread** (`addComment`): updates. Each comment is an append-only, point-in-time record of progress: what changed, what was learned or decided, and what comes next. - **Links** (`addLink`): references to resources the body or thread uses or refers to, such as pull requests, documents, dashboards, and agent sessions. The label names the resource. Add every relevant pull request with `kind: "pr"`; mentioning a PR only in prose is not enough. - Use references for relationships between Topics. Do not rely on pasted fids or prose scanning.