# Domains

> Scope-local packs under domains/<name>/, enabled_domains in config.yaml, domain-scoped kind overrides, and when to propose a domain vs a note.

- Repository: superdesigndev/loopany
- GitHub: https://github.com/superdesigndev/loopany
- Human docs: https://grok-wiki.com/public/docs/superdesigndev-loopany-97bd9ab97ae8
- Complete Markdown: https://grok-wiki.com/public/docs/superdesigndev-loopany-97bd9ab97ae8/llms-full.txt

## Source Files

- `src/core/config.ts`
- `src/commands/domain.ts`
- `src/core/engine.ts`
- `src/core/kind-registry.ts`
- `skills/loopany-core/conventions/taxonomy.md`
- `CLAUDE.md`

---

---
title: "Domains"
description: "Scope-local packs under domains/<name>/, enabled_domains in config.yaml, domain-scoped kind overrides, and when to propose a domain vs a note."
---

Domains in loopany combine two mechanisms: a **scope tag** on artifact frontmatter (`domain: <name>`) and optional **domain packs** under `$LOOPANY_HOME/domains/<name>/kinds/` whose kind definitions load only when the name appears in `config.yaml` → `enabled_domains`. Artifacts always live in the global `artifacts/` pool; enabling a domain changes validation and `loopany kind list`, not where files are stored.

## Two layers: tag vs pack

| Layer | What it is | Runtime effect |
|-------|------------|------------------|
| **Domain tag** | Optional `domain` frontmatter on any artifact (built-in field, not per-kind schema) | Indexed for `artifact list --domain`, `followups --domain`, `refs --domain`, `search --domain`; surfaced in factory graph payload |
| **Domain pack** | Directory `$LOOPANY_HOME/domains/<name>/kinds/*.md` | Extra kind definitions merged into `KindRegistry` when `<name>` is enabled |

Storage stays flat under `artifacts/<dirName>/<id>.md`. The product doc in `CLAUDE.md` also describes `manifest.yaml`, `settings.yaml`, `view.json`, and `daily/` under each domain — those paths are **design targets**, not loaded by the current CLI. Today only `domains/<name>/kinds/` is wired in `bootstrap()`.

```mermaid
flowchart TB
  subgraph workspace["$LOOPANY_HOME"]
    config["config.yaml\nenabled_domains"]
    globalKinds["kinds/*.md\ncore kinds"]
    artifacts["artifacts/**\nflat pool + domain tag"]
    pack["domains/{name}/kinds/*.md\ndomain kinds"]
  end
  bootstrap["bootstrap() in engine.ts"]
  registry["KindRegistry"]
  config --> bootstrap
  globalKinds --> bootstrap
  pack -->|"only if name ∈ enabled_domains"| bootstrap
  bootstrap --> registry
  registry --> artifacts
```

## Workspace layout

After `loopany init`, the workspace has `kinds/`, `artifacts/`, and `config.yaml`. Init does **not** create `domains/`; you add packs manually when a scope matures.

:::files
$LOOPANY_HOME/
  config.yaml                 # schemaVersion, enabled_domains
  kinds/                      # core kinds (always loaded)
  artifacts/                  # all artifacts; optional domain: in frontmatter
  domains/
    crm/
      kinds/
        deal.md               # example domain-only kind
:::

<Info>
One artifact can carry a `domain` tag without any pack directory existing. Conversely, you can enable a domain name before writing `domains/<name>/kinds/` — missing pack dirs are skipped silently at load time.
</Info>

## Configuration: `enabled_domains`

`Config` reads and writes `$LOOPANY_HOME/config.yaml`. The only domain-related key today is `enabled_domains` (YAML array of strings, sorted on write).

<ParamField body="enabled_domains" type="string[]">
Names of domain packs whose `kinds/` directories are merged at engine startup. Defaults to `[]` when absent.
</ParamField>

Fresh workspaces get `schemaVersion` only; domains start disabled:

```yaml
# loopany init default
schemaVersion: "0.2.0"
```

Example after enabling CRM and ads:

```yaml
schemaVersion: "0.2.0"
enabled_domains:
  - ads
  - crm
```

`enableDomain` / `disableDomain` are idempotent and persist immediately. There is **no** validation that `domains/<name>/` exists — enabling is purely a config flag until the next command bootstraps the registry.

## How domain packs load kinds

On every command, `bootstrap()` builds pack paths from enabled names and passes them to `KindRegistry.load()`:

1. Load all `kinds/*.md` under the workspace root (core kinds from init).
2. For each entry in `enabled_domains`, load `domains/<name>/kinds/*.md` if that directory exists.
3. Register each kind once; duplicates (same `kind` or `dirName` from a second file) become registry **issues**, not overrides.

Global kinds win precedence in practice: they load first, so a domain file reusing an existing `kind` name is rejected with a duplicate-kind issue.

<Warning>
Disabling a domain removes its kinds from the registry on the next CLI invocation. Existing artifact **files** remain on disk, but operations that need a registered kind (for example `artifact create --kind deal`) fail until you re-enable. `loopany doctor` may warn when artifacts reference kinds that are no longer loaded.
</Warning>

### Example domain kind (`deal`)

E2E tests scaffold a minimal CRM pack:

```markdown
---
kind: deal
dirName: deals
indexedFields: [status]
---
## Frontmatter
```yaml
title:  { type: string, required: true }
status: { type: enum, values: [open, won, lost] }
```
```

Workflow:

<Steps>
<Step title="Write the pack">
Create `$LOOPANY_HOME/domains/crm/kinds/deal.md` (and any sibling kind files).
</Step>
<Step title="Enable the domain">
```bash
loopany domain enable crm
```
</Step>
<Step title="Verify kinds">
```bash
loopany kind list
```
`deal` appears only after enable.
</Step>
<Step title="Create scoped artifacts">
```bash
loopany artifact create --kind deal --slug acme-q2 --title "Acme Q2" --status open --domain crm
```
</Step>
</Steps>

## Domain tag on artifacts

`domain` is a **built-in** store field accepted on every kind without appearing in each kind’s Frontmatter schema. Set at create time with `--domain`, or later with `artifact set <id> --field domain --value <name>`.

Core kinds such as `task`, `signal`, `brief`, `mission`, `learning`, `note`, and `person` also declare optional `domain` in their kind specs and often list `domain` in `indexedFields` where filtering matters.

The tag is organizational metadata:

- Does **not** move files into `domains/`
- Does **not** auto-enable a domain pack
- Can differ from pack name in theory, but convention is to align tag and pack (e.g. both `crm`)

## CLI commands

| Command | JSON shape | Behavior |
|---------|------------|----------|
| `loopany domain list` | `{ enabled, observed_only }` | `enabled` from config; `observed_only` = domain values seen on artifacts but not enabled |
| `loopany domain enable <name>` | `{ enabled }` | Appends name to `enabled_domains`, sorted |
| `loopany domain disable <name>` | `{ enabled }` | Removes name; no error if missing |

<RequestExample>
```bash
loopany domain list
```
</RequestExample>

<ResponseExample>
```json
{
  "enabled": ["crm"],
  "observed_only": ["ads"]
}
```
</ResponseExample>

`observed_only` helps you spot informal tagging before you enable packs — for example five tasks already carry `domain: sales` but `sales` is not in `enabled_domains`.

## Queries and filters

Commands that accept `--domain <name>` filter on the artifact frontmatter tag:

| Command | Filter semantics |
|---------|------------------|
| `artifact list --domain D` | Index `byDomain(D)` |
| `followups --due … --domain D` | Due items where `frontmatter.domain === D` |
| `search <query> --domain D` | Search index `domain` column |
| `refs <id> --domain D` | Keeps edges where **both** endpoints have `domain: D` |

`refs --domain` is stricter than list/search: cross-domain edges are dropped unless both artifacts share the tag.

## Doctor: domain coverage

`loopany doctor` includes a **domain coverage** check (warning, not fail). Any artifact with a `domain` string not present in `enabled_domains` is listed, e.g. `my-task: domain "unknown-d" not in enabled_domains`.

<Tip>
After you start tagging artifacts informally, run `domain list` and `doctor` together: enable packs you are ready to support, or remove stray tags.
</Tip>

## Factory UI

`loopany factory` builds a graph payload with:

- `domains` — distinct non-null `domain` values on nodes
- `enabledDomains` — copy of `config.enabled_domains`

The UI uses these for labeling; it does not read `domains/*/settings.yaml`.

## When to propose a domain vs a note

Default decision path lives in `skills/loopany-core/conventions/taxonomy.md` (skill: **new-concept**). Short version:

**Write a `note` first** when “I want to track X” and no existing kind fits — unless structure pays back (state machine, dedup identity, typed queries, fixed body sections).

**Propose a domain** only when usage shows a **separable scope** — a vertical (sales, ads), a rhythm (weekly review), or a workflow that wants its own kinds/settings/skills, and that does not belong in the global pool.

Practical signals from taxonomy:

- ≥ **5** artifacts already use the same informal `domain: <name>`, **or**
- A new kind would **only** make sense inside one scope

**Prefer domain over orphan domain kind:** if torn between “new kind globally” and “new domain that owns the kind”, choose the domain so vertical kinds stay in `domains/<name>/kinds/`.

| Situation | Recommendation |
|-----------|----------------|
| One-off preference (“client likes async standups”) | `note` |
| Published entity with identity + metrics (`post`, `customer`) | Domain kind inside e.g. `content` or `crm` |
| Cadence/workstream (“weekly investor update”) | Domain reusing core `brief` + `task`, not a new core kind |
| Vertical entity (`deal`, `order`) | `kind` in domain pack, not core `kinds/` |

Domain scaffolding after user agreement is still maturing in skills: surface the proposal, then create `domains/<name>/kinds/` and run `domain enable`.

### Anti-patterns

- Proposing a **core** kind for verticals (`deal`, `recipe`, `campaign`) — belongs in a domain pack.
- Creating a **domain** when an extra field on a `note` suffices.
- Using **`task`** to hold entities (tweets, customers) — use `note` or a domain kind; link with graph relations.

## Core kinds vs domain kinds

| | Core kinds (`kinds/`) | Domain kinds (`domains/<n>/kinds/`) |
|--|----------------------|-------------------------------------|
| Shipped | Bundled at `loopany init` from `skills/loopany-core/kinds` | Never pre-shipped; user/agent authored |
| Load rule | Always | Only when domain enabled |
| Purpose | Agent lifecycle slots (`task`, `signal`, `mission`, …) | Vertical entities (`deal`, `contact`, …) |

Promotion flow for a new domain kind mirrors global kinds: author `domains/<name>/kinds/<kind>.md`, enable domain, use `loopany artifact create --kind <kind> …`. Global kind proposal via `loopany kind propose` applies to workspace `kinds/`, not domain paths.

## Mental model: storage vs organization

```text
artifacts/          ← canonical pool (immutable write rules)
domains/<name>/       ← scope-local kind definitions (+ planned config)
config.yaml           ← which packs are active
frontmatter.domain    ← lightweight scope label on any artifact
```

An artifact can be tagged `domain: crm` while also referenced from graph edges to mission-wide artifacts with no domain tag. Multiple domains do not duplicate storage — they filter views and extend validation.

<Note>
`src/core/domain-loader.ts` is listed in project docs as a future module; loading is inline in `engine.ts` today. Do not expect manifest/settings files to affect CLI behavior until a later release wires them.
</Note>

## Verification checklist

<Steps>
<Step title="List domains">
`loopany domain list` — confirm `enabled` and any `observed_only` names.
</Step>
<Step title="Enable and list kinds">
`loopany domain enable <name>` then `loopany kind list` — pack kinds appear.
</Step>
<Step title="Create tagged artifact">
`loopany artifact create --kind task --title "..." --status todo --domain <name>` then `artifact list --domain <name>`.
</Step>
<Step title="Doctor">
`loopany doctor` — domain coverage should be `ok` when every used tag is enabled.
</Step>
</Steps>

## Related pages

<CardGroup>
<Card title="Kinds and validation" href="/kinds-and-validation">
Markdown kind definitions, dynamic schemas, and immutable artifact writes — including how domain packs register kinds.
</Card>
<Card title="Configuration reference" href="/configuration-reference">
`config.yaml` keys, `LOOPANY_HOME`, and schema version guards.
</Card>
<Card title="Artifact commands" href="/artifact-commands">
`--domain` on create, `artifact set`, and list filters.
</Card>
<Card title="CLI reference" href="/cli-reference">
Full `loopany domain` subcommand surface and JSON stdout conventions.
</Card>
<Card title="Doctor and troubleshooting" href="/doctor-and-troubleshooting">
Domain coverage warnings and disabled-pack edge cases.
</Card>
</CardGroup>
