Skip to Content
Documentation

Skill: Create a Custom Document

For AI coding agents: This page is a dense reference checklist optimized for tools like Claude Code scaffolding Loopstack code. For the human-readable guide, see Creating Documents.

Document Anatomy

A document is a plain TypeScript DTO decorated with @Document(). It pairs a Zod schema (for content validation) with an optional YAML widget config (for Studio rendering).

import { z } from 'zod'; import { Document } from '@loopstack/common'; export const NotesSchema = z.object({ text: z.string(), }); @Document({ schema: NotesSchema, widget: __dirname + '/notes.ui.yaml', }) export class NotesDocument { text: string; }
# notes.ui.yaml type: document ui: widgets: - widget: form options: properties: text: title: Notes widget: textarea rows: 8

Documents are NOT NestJS providers. Unlike @Tool and @Workflow, @Document does not apply @Injectable(). Do not add document classes to a module’s providers array. Do not inject them. Reference the class directly: documentStore.save(NotesDocument, …).

@Document() Options

All options are optional.

OptionTypeDefaultDescription
namestringclass name with Document suffix stripped, snake_casedExplicit snake_case identifier. E.g. AskUserDocumentask_user.
titlestringDisplay title shown in Studio.
descriptionstringDescription shown in Studio.
widgetWidgetRef | WidgetRef[]YAML file path(s) — or inline widget object(s) — defining how the document renders.
schemaz.ZodTypeZod schema validating document content on documentStore.save().
tagsstring[]Default tags assigned to every instance.
metaStaticDocumentMetaStatic metadata served via the config endpoint, not persisted per instance.

Saving Documents

documentStore is auto-injected on BaseWorkflow and BaseTool. Reference document classes directly — no injection.

// Create a new document await this.documentStore.save(NotesDocument, { text: 'Hello!' }); // Create or update with a specific ID (idempotent) await this.documentStore.save(NotesDocument, { text: 'Updated' }, { id: 'notes-1' }); // Hidden from the UI (still persisted) await this.documentStore.save(NotesDocument, { text: 'Internal' }, { meta: { hidden: true } });

Save an instance instead

const draft = this.documentStore.create(NotesDocument, { text: 'Initial' }); draft.text += '\n\nMore.'; await this.documentStore.save(draft); // overload: pre-built instance await this.documentStore.save(draft, { id: 'notes-1' }); // with save options

create() returns a typed class instance with the data attached. Nothing is persisted until save().

Save Options

OptionTypeDescription
idstringCustom ID — passing the same ID twice updates the same record.
meta.hiddenbooleanHide the document from the Studio UI.

Querying Documents

All methods return only non-invalidated documents for the current run.

MethodReturnsUse for
findAll(MyDocument)MyDocument[]Typed hydrated instances of one document type.
findByTag('tag')DocumentEntity[]All documents tagged with tag (across types).
findAllDocuments()DocumentEntity[]Every document in this run, raw entities.
const notes = this.documentStore.findAll(NotesDocument); // typed const tagged = this.documentStore.findByTag('message'); const all = this.documentStore.findAllDocuments();

documentStore.create(MyDocument, data) returns a typed instance without persisting — validates against the Zod schema only.

Reusing Built-in Documents

Don’t redefine these — import and save:

DocumentSourceKey fields
LlmMessageDocument@loopstack/llm-provider-modulerole, text, blocks
LinkDocument@loopstack/commonlabel, workflowId, embed, expanded
MessageDocument@loopstack/commonrole, text
MarkdownDocument@loopstack/commonmarkdown
PlainDocument@loopstack/commontext
ErrorDocument@loopstack/commonerror
import { MarkdownDocument } from '@loopstack/common'; import { LlmMessageDocument } from '@loopstack/llm-provider-module'; await this.documentStore.save(LlmMessageDocument, { role: 'assistant', text: 'Hello!' }); await this.documentStore.save(MarkdownDocument, { markdown: '# Report\n- A\n- B' });

YAML Widget Config

The widget option points to a YAML file describing how the document renders in Studio. The most common widget is form:

type: document ui: widgets: - widget: form options: order: [name, description, items] properties: name: title: Name description: title: Description widget: textarea items: title: Items collapsed: true items: title: Item actions: - type: button transition: submit label: 'Submit'

Widget Types

Used in options.properties.<field>.widget:

WidgetDescription
textSingle-line text (default)
textareaMulti-line text
selectDropdown
radioRadio buttons
checkboxCheckbox
switchToggle
sliderNumeric slider
code-viewSyntax-highlighted read-only code

Property Options

OptionTypeDescription
titlestringDisplay label
widgetstringWidget type (see above)
placeholderstringPlaceholder text
rowsnumberVisible rows (textarea)
readonlybooleanRead-only field
hiddenbooleanHide the field
disabledbooleanDisable interaction
collapsedbooleanCollapse arrays/objects by default
itemsobjectUI config for array items

Actions

Buttons that trigger wait: true transitions:

actions: - type: button transition: confirm # must match the transition method name label: Confirm

Checklist

  1. Decide if a built-in document covers your need first — don’t reinvent MessageDocument, MarkdownDocument, etc.
  2. Create the class file with @Document({ schema, widget }).
  3. Define the Zod schema next to or above the class.
  4. Add the YAML widget config beside the .ts file (e.g. notes.ui.yaml).
  5. Do not add the class to module providers. Reference it directly via documentStore.save(MyDocument, …).
  6. Save via documentStore.save(MyDocument, content, options?) from a workflow transition or tool.
Last updated on