Skip to content

Custom Schema

By default, a Tiroir store is organised like the bundled accounting-firm example: one drawer per client. If your team works differently, you describe your own organisation in a file, the schema, and Tiroir does the rest. This page takes you from nothing to a working store with your own drawers. Count about 30 minutes for a first try.

No guided setup in 0.1.0

You write the schema by hand, on the server. A guided init, where Claude interviews a member in their language and proposes the schema, is planned (see the Roadmap) and does not exist yet. Until then, a custom schema is a preset of your own: a folder on the server that a store is created from.

The ideas in five minutes

Tiroir organises memory the way people organise paper files, and keeps it as plain Markdown files in a Git repository.

Term What it is Example
Store One memory, one Git repository. A store is either personal, owned by one member, or team, shared by the members you grant. my-team-memory
Drawer One "thing" your memory is about: one client, one code repository, one engagement. A drawer is a folder. the drawer of the billing-api repository
Drawer type The kind of a drawer. All the drawers of a type are organised the same way. repo, client
Summary The drawer's current state: a short document of named sections, rewritten as things change. what billing-api is for, its architecture
Log The drawer's history: one small dated file per event, never edited afterwards. "2026-10-21: migrated invoices to the new table"
Shared space Documents that belong to no drawer: conventions, procedures, calendars. shared/conventions.md
Schema The file tiroir.yaml that declares the drawer types and how each one is organised. this page
Preset A folder holding a schema, its templates, shared documents and, optionally, starting drawers. my-team

A store with one drawer looks like this on disk:

my-team-memory/
  tiroir.yaml                  the schema
  MEMORY.md                    the table of contents, generated by tiroir (see step 5)
  templates/                   what a new summary or log entry looks like
  shared/conventions.md        a shared document
  repos/billing-api/           one drawer
    summary.md                 its summary
    log/2026/10/…md            its log, one file per entry

Before you write anything: three questions

Take a sheet of paper and answer:

  1. What are your drawers? The things your team keeps coming back to: clients, repositories, projects, patients, suppliers. Start with one type.
  2. What do you want to remember about each one? List the headings of the summary, in the order you would read them. Four to six is plenty. These become the summary sections.
  3. What identifies a drawer? A code, a name, a slug. This becomes the drawer's fields, and its folder name.

Two rules of thumb

  • What is true now goes in the summary; what happened goes in the log. "The service runs on PostgreSQL" is a summary fact. "We moved to PostgreSQL on 21 October" is a log entry.
  • A drawer is something people name in conversation. If your team says "have you seen the Dupont file?" or "what about billing-api?", then clients or repositories are your drawers.

The rest of the page builds a store for a team that wants one drawer per code repository. Replace "repository" with your own answer as you go.

Step 1: Prepare the presets folder

The presets/ folder of the repository you cloned in the Installation Guide holds the bundled presets, the same ones as the image. You will add yours next to them. From the directory that holds compose.yaml:

mkdir -p presets/my-team/templates presets/my-team/shared

presets/accounting-firm is a complete example of every file below, so keep it open as you read. my-team is the name you will give to --preset: lowercase letters, digits and hyphens, starting with a letter or a digit.

The server reads its presets from inside the container, so tell Compose to use this folder instead of the one in the image. Create compose.override.yaml next to compose.yaml; Compose reads it automatically:

services:
  tiroir:
    volumes:
      - ./presets:/app/presets:ro
docker compose up -d

From now on, a change to a file under presets/ is seen by the server at once; no restart is needed.

Step 2: Write the schema

Create presets/my-team/tiroir.yaml:

# Version of the file format. Always 1.
tiroir: 1

# The identity of the store. You do not choose it here: `stores add` replaces
# id, name, audience and language with what you type on the command line.
# Only `description` is kept. The other values just have to be valid.
store:
  id: 00000000-0000-0000-0000-000000000000
  name: placeholder
  audience: team
  language: en
  description: "What our team knows about its repositories."

# The shared space: documents that belong to no drawer.
shared:
  topics: [conventions, release process]

# The kinds of drawer. One entry per kind; the key is the type's name.
drawer_types:
  repo:
    label: Repository
    label_plural: Repositories
    description: "One code repository of the team."
    # Where a drawer lives. {name} is replaced by the drawer's `name` field.
    path: "repos/{name}"
    # The drawer's identity card: facts that are not prose.
    fields:
      name:
        type: text
        required: true
        pattern: "[a-z0-9-]+"
        description: "The repository's name, in lowercase, such as billing-api."
      aliases:
        type: list
        description: "Other names people use for it."
      language:
        type: enum
        values: [python, typescript, go, other]
        description: "The main language."
    # The summary: a template, and the headings it is made of, in order.
    summary:
      template: templates/repo-summary.md
      sections: [Purpose, Architecture, Conventions, Open questions]
    # What a new log entry looks like.
    log:
      template: templates/repo-log.md
    # Optional: how a drawer is recognised in a conversation, and what is
    # put before Claude when it comes up.
    detect:
      names: [name, aliases]
    inject:
      sections: [Purpose, Conventions]
      max_tokens: 800

Read it from the top:

  • tiroir: 1 is the version of the file format. Always 1.
  • store is a placeholder. When you create a store, Tiroir generates its id and takes name, audience and language from the command line, so what you write there is replaced. Keep the block, with valid values, and write a description: it is the one line kept in the store.
  • shared.topics says what the shared space is about. It is a hint for Claude, not a list of files.
  • drawer_types is the heart of the file: one entry per kind of drawer, here repo. The key (repo) is the type's name.
    • label, label_plural, description are the words used when Tiroir talks about the type. Write them in the store's language.
    • path says where a drawer lives. repos/{name} means that the drawer of a repository whose name is billing-api is the folder repos/billing-api. The placeholder must be a required field, and it is what makes each drawer unique.
    • fields is the drawer's identity card, the information that is not prose. Each field has a type: text, number, date (2026-10-21), boolean, enum (one of the values you list) or list (a list of text). required: true makes it mandatory, and pattern restricts a text field to a regular expression.
    • summary names the template of a new summary and its sections, the level-2 headings, in order. They are the units that Tiroir finds, reads and, later, merges.
    • log names the template of a new log entry. Optional.
    • detect.names lists the fields whose values designate the drawer in a conversation. Optional. Most of what uses it arrives with the next release (see the Roadmap), so in 0.1.0 you can leave it out or fill it in now.
    • inject.sections and max_tokens say which sections are put before Claude when the drawer comes up, and how much. Optional, with the same remark.

Then the two templates. A summary template holds exactly the sections you declared, in the same order, with a line of guidance under each. Create presets/my-team/templates/repo-summary.md:

# {name}

## Purpose

What this repository does, and for whom.

## Architecture

The main parts and how they fit together.

## Conventions

What to know before changing the code.

## Open questions

What is still undecided.

{name} is replaced by the drawer's field of that name when it is created. The log template is the shape of a new entry. Create presets/my-team/templates/repo-log.md:

**What happened:** one sentence.

**Next step:** who does what, and by when.

Finally a shared document, presets/my-team/shared/conventions.md. Claude reads it the first time it uses the store in a session, so put in it what you want every entry to respect:

---
title: Team conventions
---
# Team conventions

## Writing

Write in English, in short sentences.

## Secrets

Never write a password or a token in memory.

Step 3: Give it a first drawer

An important limitation of 0.1.0

A member cannot create a drawer from Claude: Claude can search, read and add log entries, nothing more. Every drawer you want must therefore exist when the store is created, and there is no way to add one afterwards. This will be lifted: creating drawers from Claude is planned (see the Roadmap). Until then, plan your first drawers carefully.

You give the store its drawers in a data/ folder of the preset, laid out exactly as in the store, one folder per drawer.

The same limit applies to what is written in a drawer. You write the summary, here, in the preset, and nothing completes it later: in 0.1.0, neither Claude nor a member can change a summary, so write what you know, as completely as you can. Claude reads it to know the drawer. The log, by contrast, grows: members ask Claude to add entries, each one a new dated file. Both are searched. Updating summaries from Claude is planned too, and a person will confirm each update (see the Roadmap).

Create the first drawer's summary, presets/my-team/data/repos/billing-api/summary.md:

---
type: repo
name: billing-api
aliases: [billing, invoices-api]
language: python
---
# billing-api

## Purpose

Issues invoices and records payments for the web shop.

## Architecture

A FastAPI service in front of a PostgreSQL database.

## Conventions

Every change needs a migration and a test.

## Open questions

Do we keep the legacy CSV export?

The top block, between the two --- lines, holds the drawer's fields: type is the name of the drawer type, and the others are the fields you declared. Then come the sections, with exactly the headings of the schema. Add one folder like this per drawer.

Keep to the schema

In 0.1.0, Tiroir checks tiroir.yaml when it creates a store, but it does not check the drawers of data/: a misspelled section or a value that is not in an enum goes through. Copy the template headings exactly.

Step 4: Create the store

Try it on a machine that does not matter first, for example your laptop with the same compose.yaml: 0.1.0 has no command to delete a store, so a store you created by mistake stays there.

docker compose exec tiroir tiroir admin stores add my-team-memory \
  --audience team --language en --preset my-team

The command prints store: my-team-memory. A team store has no member yet: give access to the members you created (see the Installation Guide).

docker compose exec tiroir tiroir admin grant alice my-team-memory write
docker compose exec tiroir tiroir admin grant bob my-team-memory read
docker compose exec tiroir tiroir admin stores list

If the schema is wrong, nothing is created, and every problem is listed with its location:

error: schema.invalid: the schema has 1 violation:
schema.invalid: drawer_types.repo.fields.language.type (line 35): expected one of: text, number, date, boolean, enum, list

The key path (drawer_types.repo.fields.language.type) and the line say where to look. Fix the file and run the command again.

Message Meaning
schema.invalid A key is unknown, misspelled, mis-indented or of the wrong type. An unknown key is always an error, so that a typo never silently disables a rule.
schema.invalid-path A drawer type's path has no placeholder of a required field.
schema.unknown-section inject.sections or a template names a heading that is not in summary.sections.
preset.unknown There is no folder of that name in presets/, or the name has capitals or spaces.
store.exists A store with that name exists already.

Step 5: Look at the result

The store is a Git repository inside the container, in a folder named by an id, not by the store's name. Find yours by its title:

docker compose exec tiroir sh -c '
  for repo in /data/stores/*.git; do
    echo "$repo: $(git --git-dir="$repo" show main:MEMORY.md | head -1)"
  done'
/data/stores/6f0c5a3e-….git: # my-team-memory

Then print the table of contents that Tiroir generated, MEMORY.md, and the list of files, with the folder you found:

docker compose exec tiroir git --git-dir=/data/stores/<id>.git show main:MEMORY.md
docker compose exec tiroir git --git-dir=/data/stores/<id>.git ls-tree -r --name-only main

MEMORY.md looks like this:

# my-team-memory

What our team knows about its repositories.

Each drawer is a folder holding `summary.md`, its summary, and `log/`, its dated log entries. Shared documents are under `shared/`.

## Repositories

- [[repos/billing-api/summary]] — Repository: billing-api, billing, invoices-api

What is MEMORY.md?

It is the store's table of contents: its name, a short explanation of the layout, and one line per drawer. Tiroir generates it from the drawers, so you never write it, and a hand edit would be overwritten. It exists so that a person can open the store without Tiroir, in a text editor or as an Obsidian vault, and find their way around.

Then connect as alice with Claude Desktop and try, in that order:

Which drawers do I have in my-team-memory?

Show me the summary of billing-api.

Add a log entry to billing-api: "Moved the invoice export to a nightly job."

Claude should list the drawer, read its summary and writes the entry as a new dated file under repos/billing-api/log/. If all three work, your schema works.

Going further

  • A second drawer type. Add another entry under drawer_types (project, incident), with its own path, fields, templates and sections. Two types must not use overlapping paths: repos/{name} and repos/{name}/docs would clash.
  • A different language. language is the memory language, for example fr. Write the labels, the section headings and the templates in that language; --language on stores add overrides the one in the file.
  • Learn from a complete example. presets/accounting-firm is the full version of what you built: a French schema with six fields, six sections, templates, shared procedures and 23 drawers with their logs.
  • Every key, every rule. The Schema Reference lists them all, with the error each one gives.

Good to know

  • A preset is a starting point. It is copied when the store is created. Changing the preset afterwards does not change the stores already made from it, and 0.1.0 has no command to change the schema of an existing store; schema (see the Roadmap). Settle the schema on a test store before the real one.
  • Upgrades. With ./presets mounted, the server reads the presets of your clone, not those of the image. A git pull brings the new bundled presets and leaves yours alone.

See the CLI Reference for stores add and grant, and the Installation Guide for the stack.