# 8. Filesystem Projections (`[FS]`)
## Overview
By default, FUSE explodes a pattern's result cell into a recursive directory
tree. This works well for structured data but is poor UX for patterns whose
primary output is a document — you get dozens of nested directories instead
of a readable file.
The `[FS]` symbol lets a pattern declare its own on-disk representation.
When a result cell includes `[FS]`, FUSE produces a single projection file
(`index.md` or `index.json`) at the piece root instead of a `result/`
directory.
## Declaring a Projection
Import `FS` and `FsProjection` from `commonfabric` and add `[FS]` to the
pattern's return object:
```tsx
// Shown at module scope.
import { FS, type FsProjection, NAME, pattern, UI } from "commonfabric";
const MyPattern = pattern(({ title, content }) => {
// ...
return {
[NAME]: computed(() => `My Pattern`),
[UI]: myUI,
[FS]: {
type: "text/markdown",
frontmatter: { title },
content,
},
// other fields...
};
});
```
## The `FsProjection` Type
```ts
type FsProjection =
| {
type: "text/markdown";
frontmatter?: Record;
content: string;
}
| {
type: "application/json";
content: Record;
}
| Record; // plain object → default JSON projection
```
## Projection Variants
### `text/markdown` — Markdown with YAML Frontmatter
Produces `index.md` at the piece root:
```markdown
---
entityId: of:ba4jcbvpq3k5soo...
title: My Note Title
---
The note body content goes here.
```
- `entityId` is always injected first and is read-only.
- Primitive `frontmatter` fields (string, number, boolean, null) become YAML
key-value pairs.
- Complex `frontmatter` fields (arrays of entities, nested objects) cannot be
represented in YAML — they become sibling directories alongside `index.md`,
using the same directory-tree rules as the default result layout.
- `content` becomes the markdown body after the closing `---`.
### `application/json` — Explicit JSON
Produces `index.json` at the piece root:
```json
{
"entityId": "of:ba4jcbvpq3k5soo...",
"field1": "value1",
"field2": 42
}
```
`entityId` is always injected first. The `content` object's keys follow.
### Plain Object (Default JSON Shorthand)
Omitting `type` treats the entire `[FS]` value as the content of
`index.json`. Equivalent to `{ type: "application/json", content: theObject }`:
```tsx
// Shown for illustration only.
[FS]: { summary, count } // → index.json with { entityId, summary, count }
```
## Write-Back
`index.md` and `index.json` are writable. Edits are parsed and written back
to the corresponding reactive cells.
| File | Write-Back Behavior |
|------|---------------------|
| `index.md` | Parses YAML frontmatter → updates `$FS.frontmatter.` cells; parses body → updates `$FS.content` cell |
| `index.json` | Parses JSON → updates `$FS.content.` cells |
`entityId` is always skipped on write (read-only).
## Callables and `.handlers`
When `[FS]` is active, callable files (`.handler`, `.tool`) move from
`result/` to the piece root, alongside `index.md` or `index.json`.
The `.handlers` summary file is always at the piece root regardless of
whether `[FS]` is used.
## The `.handlers` File
Every piece has a `.handlers` file at its root, generated automatically:
```
editContent.handler {
detail: {
value: string
}
}
setTitle.handler string
appendLink.handler {
piece: MentionablePiece
}
createNewNote.handler void
```
- Dot-prefixed: hidden from `ls` but readable with `cat .handlers`
- One entry per callable (handlers and tools)
- Input type shown as a TypeScript-ish shape
- Void handlers (no payload) show `void`
## Callable Scripts
Each `.handler` and `.tool` file embeds the input schema as readable comments:
```sh
#!/path/to/cf-exec exec
# schema: {"type":"string"}
# input: string
exec '/path/to/cf-exec' exec "$0" "$@"
```
Use `cat setTitle.handler` or `head setTitle.handler` to see the expected
input before invoking. Run with `--help` for full usage including all flags:
```bash
./setTitle.handler --help
# Usage:
# ./setTitle.handler [invoke] --value
# ...
# Input type:
# string
# Flags:
# --value Required.
```
Call with no arguments to see the expected type in the error:
```bash
./setTitle.handler
# Handler requires input. Expected type: string
# Run --help for full usage.
```
## Live Updates
Projection files update reactively. When a cell changes, the FUSE daemon
regenerates `index.md`/`index.json` in place and invalidates the kernel
cache. Reads always see the current cell value.
## Example: Note Pattern
The `Note` pattern in `packages/patterns/notes/note.tsx` uses `[FS]`:
```tsx
// Shown inside a pattern body.
return {
[NAME]: computed(() => `📝 ${title.get()}`),
[UI]: ...,
[FS]: {
type: "text/markdown",
frontmatter: { title },
content,
},
title,
content,
editContent,
setTitle,
// ...
};
```
Mounted result for a note piece:
```
home/pieces/📝 My Note/
index.md # YAML frontmatter + note body
$UI.json # serialized UI tree (single file, not exploded)
editContent.handler # { detail: { value: string } }
setTitle.handler # string
appendLink.handler # { piece: MentionablePiece }
createNewNote.handler # void
toggleMenu.handler # void
input.json
input/
title
content
meta.json
.handlers
```
---
**Previous:** [Open Questions](./7-open-questions.md)