Docs/Core SDK/Path DSL

    Path DSL

    The S3WORM Path DSL is a mini-language for describing the structure of S3 keys. It defines how entity data is organized inside a bucket -- namespaces, collections, dynamic segments (IDs), and required files.

    A path like #org/@customers/(id:uuid)/[profile].json maps to S3 keys like org/customers/a1b2c3d4-e5f6-7890-abcd-ef1234567890/profile.json.

    Symbols

    The DSL uses four symbols to describe path segments. Each symbol is stripped during resolution, leaving clean S3 keys.

    SymbolNameSyntaxDescription
    #Namespace#nameA fixed organizational prefix. Groups related collections.
    @Collection@nameA collection of entities. The directory that holds entity folders.
    ()Dynamic Segment(name:type)A variable segment, typically an entity ID. Replaced with actual values at runtime.
    []Required File[name].extA required document file within an entity folder.

    Namespace: #

    Namespaces are fixed prefixes that organize your bucket into logical sections:

    #org/@customers/...     -> org/customers/...
    #data/@logs/...         -> data/logs/...
    #billing/@invoices/...  -> billing/invoices/...
    

    Multiple namespaces can be nested:

    #app/#v2/@users/...     -> app/v2/users/...
    

    Collection: @

    Collections mark directories that contain multiple entities:

    #org/@customers/(id:uuid)/[profile].json
          ^^^^^^^^^
          This is the collection directory.
          S3 key prefix: org/customers/
    

    Dynamic Segment: ()

    Dynamic segments represent variable path parts -- usually entity IDs. The syntax is (name:type) where name is a label and type is a dynamic type:

    #org/@customers/(id:uuid)/[profile].json
                    ^^^^^^^^^
                    Replaced with the actual entity ID at runtime.
    

    When resolving the base path for a collection, the StorageResolver stops at the first dynamic segment. Everything before it becomes the S3 prefix for listing entities.

    Required File: []

    Required files specify the document filename for an entity:

    #org/@customers/(id:uuid)/[profile].json
                              ^^^^^^^^^^^^^^
                              The entity document is named "profile.json"
    

    The brackets are stripped during resolution: [profile].json becomes profile.json.

    Dynamic Types

    Dynamic segments have a type that defines the format of valid values. Each type has an associated regex pattern for validation.

    TypePatternExample Values
    uuid^[0-9a-fA-F-]{36}$a1b2c3d4-e5f6-7890-abcd-ef1234567890
    evm^0x[a-fA-F0-9]{40}$0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18
    sol^[1-9A-HJ-NP-Za-km-z]{32,44}$DRpbCBMxVnDK7maPMoaH6...

    Custom Dynamic Types

    Define additional types in the schema's dynamicTypes section:

    {
      "dynamicTypes": {
        "uuid": { "regex": "^[0-9a-fA-F-]{36}$" },
        "evm": { "regex": "^0x[a-fA-F0-9]{40}$" },
        "sol": { "regex": "^[1-9A-HJ-NP-Za-km-z]{32,44}$" },
        "slug": { "regex": "^[a-z0-9-]+$" },
        "date": { "regex": "^\\d{4}-\\d{2}-\\d{2}$" }
      }
    }
    

    These types are used in path validation and codegen. The CLI worm lint command checks that dynamic segments in paths reference defined types.

    Operators

    Operators control how dynamic segment values are generated when creating new entities.

    OperatorStrategyDescription
    autoUUID generationGenerates a random UUID for new entities. Default for uuid type.
    seqSequential incrementAssigns a monotonically increasing integer. Configured with startAt and step.

    Operator Definitions

    {
      "operators": {
        "auto": { "strategy": "uuid" },
        "seq": { "strategy": "increment", "startAt": 1, "step": 1 }
      }
    }
    

    When a model's idType is "uuid", the auto operator generates IDs automatically on save. With seq, the repository reads the current maximum sequence number and increments.

    Example Paths

    Basic Entity Collection

    Path:    #org/@customers/(id:uuid)/[profile].json
    Resolves: org/customers/{uuid}/profile.json
    Example:  org/customers/a1b2c3d4-e5f6-7890-abcd-ef1234567890/profile.json
    
    • #org -- namespace prefix, becomes org/
    • @customers -- collection directory, becomes customers/
    • (id:uuid) -- entity ID slot, replaced with a UUID
    • [profile].json -- document filename, becomes profile.json

    Multiple Files Per Entity

    An entity can have multiple required files:

    Path 1: #org/@customers/(id:uuid)/[profile].json
    Path 2: #org/@customers/(id:uuid)/[invoices].json
    

    Both paths share the same collection and entity ID. The entity folder contains two documents:

    org/customers/a1b2c3d4-.../
      profile.json
      invoices.json
    

    Singleton (No Dynamic Segment)

    Path:    #org/@settings/[config].json
    Resolves: org/settings/config.json
    

    No dynamic segment means there is exactly one document at this path. In the schema, mark this model as "singleton": true.

    EVM Address Collection

    Path:    #wallets/@accounts/(addr:evm)/[balance].json
    Resolves: wallets/accounts/{evm-address}/balance.json
    Example:  wallets/accounts/0x742d35Cc6634C0532925a3b844Bc9e7595f2bD18/balance.json
    

    Nested Collections

    Path:    #org/@departments/(deptId:uuid)/@teams/(teamId:uuid)/[roster].json
    

    This defines a two-level hierarchy. The storage resolver stops at the first dynamic segment for the base path:

    Base path:    org/departments/
    Entity path:  org/departments/{deptId}/teams/{teamId}/roster.json
    

    Date-Partitioned Logs

    Path:    #data/@logs/(date:date)/@entries/(id:uuid)/[event].json
    

    With a custom date type (^\d{4}-\d{2}-\d{2}$):

    data/logs/2026-02-24/entries/abc-123/event.json
    

    How Paths Map to S3 Keys

    The StorageResolver class transforms path DSL into actual S3 keys:

    Base Path Resolution

    The base path is everything up to (but not including) the first dynamic segment, with symbols stripped:

    DSL PathBase Path
    #org/@customers/(id:uuid)org/customers
    #org/@settingsorg/settings
    #data/@logs/(date:iso)/@entriesdata/logs
    #billing/@invoices/(id:uuid)billing/invoices

    Entity Key Resolution

    For a specific entity, the ID is inserted and the filename is appended:

    Model DefinitionEntity IDResolved S3 Key
    path: #org/@customers/(id:uuid), file: [profile].jsonabc-123org/customers/abc-123/profile.json
    path: #org/@settings, file: [config].json, singletonN/Aorg/settings/config.json
    path: #billing/@invoices/(id:uuid), file: [data].jsoninv-001billing/invoices/inv-001/data.json

    Collection Prefix

    For listing all entities in a collection, the resolver returns the base path with a trailing slash:

    ModelCollection Prefix
    Customerorg/customers/
    Invoicebilling/invoices/
    OrgSettingsorg/settings/

    System Directory Resolution

    The storage layout ("inline", "collection", or "root") determines where oplog, snapshot, and trash directories live relative to the entity:

    Layout: "root" (default)

    .oplog/org/customers/abc-123/         -- oplog entries
    .snapshots/org/customers/abc-123/     -- snapshots
    .worm.trash/org/customers/abc-123/    -- soft-deleted data
    

    Layout: "collection"

    org/customers/.oplog/abc-123/
    org/customers/.snapshots/abc-123/
    org/customers/.worm.trash/abc-123/
    

    Layout: "inline"

    org/customers/abc-123/.oplog/
    org/customers/abc-123/.snapshots/
    org/customers/abc-123/.worm.trash/
    

    Full Schema Example

    Putting it all together in a .worm/schema.json:

    {
      "schemaVersion": "1.0",
      "sourceOfTruth": "local",
    
      "symbols": {
        "namespace": "#",
        "collection": "@",
        "dynamic": "()",
        "requiredFile": "[]"
      },
    
      "dynamicTypes": {
        "uuid": { "regex": "^[0-9a-fA-F-]{36}$" },
        "evm": { "regex": "^0x[a-fA-F0-9]{40}$" }
      },
    
      "operators": {
        "auto": { "strategy": "uuid" },
        "seq": { "strategy": "increment", "startAt": 1, "step": 1 }
      },
    
      "storage": {
        "layout": "root"
      },
    
      "paths": [
        "#org/@customers/(id:uuid)/[profile].json",
        "#org/@customers/(id:uuid)/[invoices].json",
        "#org/@invoices/(id:uuid)/[data].json",
        "#org/@settings/[config].json"
      ],
    
      "models": {
        "Customer": {
          "path": "#org/@customers/(id:uuid)",
          "idType": "uuid",
          "fields": {
            "name": { "type": "string", "required": true },
            "email": { "type": "string", "required": true },
            "status": { "type": "string", "enum": ["active", "inactive"], "default": "active" }
          },
          "file": "[profile].json",
          "mode": "readwrite",
          "oplog": true,
          "softDelete": true
        },
        "Invoice": {
          "path": "#org/@invoices/(id:uuid)",
          "idType": "uuid",
          "fields": {
            "customerId": { "type": "string", "required": true, "ref": "Customer" },
            "amount": { "type": "number", "required": true },
            "status": { "type": "string", "enum": ["draft", "sent", "paid"], "default": "draft" }
          },
          "file": "[data].json",
          "mode": "readwrite"
        },
        "OrgSettings": {
          "path": "#org/@settings",
          "singleton": true,
          "fields": {
            "orgName": { "type": "string", "required": true },
            "plan": { "type": "string", "enum": ["free", "pro", "enterprise"] }
          },
          "file": "[config].json",
          "mode": "readwrite"
        }
      }
    }
    

    The bucket structure that results from this schema:

    my-bucket/
      .worm                                    -- bucket sentinel
      .oplog/                                  -- global oplog (layout: root)
        org/customers/
          abc-123/
            0001.json
            0002.json
      .worm.trash/                             -- global trash
        org/customers/
          deleted-xyz/
            profile.json
            .worm.tombstone
      org/
        customers/
          .worm                                -- collection sentinel
          abc-123/
            profile.json                       -- Customer entity
          def-456/
            profile.json
        invoices/
          .worm
          inv-001/
            data.json                          -- Invoice entity
        settings/
          .worm
          config.json                          -- OrgSettings singleton