Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Built-in Decorators

WCL provides a set of built-in decorators for schema validation, documentation, macro transforms, and configuration semantics.

Reference Table

DecoratorTargetsArgumentsDescription
@optionalschema fieldsnoneField is not required
@requiredschema fieldsnoneField must be present (default for schema fields)
@default(value)schema fieldsvalue: anyDefault value when field is absent
@sensitiveattributesredact_in_logs: bool = trueMarks value as sensitive; redacted in log output
@deprecatedblocks, attributesmessage: string, since: string (optional)Warns when this item is used
@validate(...)attributes, schema fieldsmin, max, pattern, one_of, custom_msgValue constraints
@doc(text)anytext: stringInline documentation for the decorated item
@example { }decorator schemas, schemasblock bodyEmbedded usage example
@allow(rule)let bindings, attributesrule: stringSuppresses a specific warning
@id_pattern(glob)schemasglob: stringEnforces naming convention on block IDs
@ref(schema)schema identifier fieldsschema: stringRequires value to reference an existing block of that type
@partial_requirespartial blocksfields: list of stringsDeclares expected merge dependencies
@merge_order(n)partial blocksn: intExplicit ordering for partial merges
@openschemasnoneAllows extra attributes not declared in the schema
@child(kind, ...)schemaskind: string, min/max/max_depth: int (optional)Per-child cardinality and depth constraints
@tagged(field)schemasfield: stringNames the discriminator field for tagged variant schemas
@children(kinds)schemaskinds: list of stringsRestricts which child blocks/tables may appear inside
@parent(kinds)schemaskinds: list of stringsRestricts which parent blocks may contain this block/table
@symbol_set(name)schema fieldsname: stringConstrains a symbol-typed field to members of the named symbol set
@inline(N)schema fieldsN: intMaps positional inline arg at index N to this named field
@textschema fieldsnoneMarks a content: string field as the text block target
@merge_strategy(s)partial blockss: stringControls how partial merges resolve conflicts

@optional

Marks a schema field as not required. If the field is absent from a block, no error is raised.

schema "service" {
    debug_port: int    @optional
    log_level:  string @optional
}

@required

Marks a schema field as required. This is the default for all schema fields, but can be written explicitly for clarity.

schema "service" {
    port:   int    @required
    region: string @required
}

@default(value)

Provides a fallback value when the field is absent from the block. A field with @default is implicitly optional.

schema "service" {
    env:      string @default("production")
    replicas: int    @default(1)
    tls:      bool   @default(true)
}

The default value must be a valid WCL expression and must match the declared field type.

@sensitive

Marks an attribute’s value as sensitive. Tools and log output should redact this value.

database "primary" {
    host     = "db.internal"
    password = "s3cr3t" @sensitive
    api_key  = "change-me" @sensitive(redact_in_logs = true)
}

The optional redact_in_logs argument defaults to true.

@deprecated

Indicates that a block or attribute is deprecated. A warning is emitted when it is used.

service "legacy-api" @deprecated(message = "Use service 'api-v2' instead", since = "3.0") {
    port = 8080
}

On an attribute:

schema "service" {
    workers:  int @deprecated(message = "Use 'replicas' instead")
    replicas: int @default(1)
}

since is optional and accepts a version string.

@validate(…)

Attaches value constraints to an attribute or schema field. Multiple constraint arguments can be combined.

schema "endpoint" {
    port:    int    @validate(min = 1, max = 65535)
    env:     string @validate(one_of = ["development", "staging", "production"])
    slug:    string @validate(pattern = "^[a-z0-9-]+$")
    timeout: int    @validate(min = 1, max = 300, custom_msg = "timeout must be between 1 and 300 seconds")
}
ArgumentApplies toDescription
minint, floatMinimum value (inclusive)
maxint, floatMaximum value (inclusive)
patternstringRegular expression the value must fully match
one_ofstring, intValue must be one of the given options
custom_msganyCustom message emitted on constraint violation

@doc(text)

Attaches a documentation string to any declaration. Used by tooling and the language server to provide hover documentation.

schema "service" {
    port: int    @required @doc("The TCP port this service listens on.")
    env:  string @default("production") @doc("Deployment environment name.")
}

service "api" @doc("Main API service for the frontend.") {
    port = 8080
}

@example

Embeds a usage example directly inside a decorator_schema or schema declaration. Used by documentation generators and IDE tooling.

decorator_schema "rate_limit" {
    target    = [attribute]
    requests:  int
    window_ms: int @default(1000)

    @example {
        calls_per_second = 100 @rate_limit(requests = 100, window_ms = 1000)
    }
}

@allow(rule)

Suppresses a specific warning on a let binding or attribute. Use this when a warning is expected and intentional.

let _unused = compute_value() @allow("unused_binding")

service "api" {
    legacy_flag = true @allow("deprecated_field")
}

The rule argument is a string identifying the warning to suppress.

@id_pattern(glob)

Enforces a naming convention on block IDs for a schema. Any block whose ID does not match the glob pattern produces a validation error (E077).

schema "service" @id_pattern("svc-*") {
    port: int
}

service "svc-api" { port = 8080 }    // valid
service "api"     { port = 8080 }    // error E077: ID does not match "svc-*"

@ref(schema)

Applied to an identifier field in a schema. Requires that the field’s value matches the ID of an existing block of the named type.

schema "deployment" {
    service_id: string @ref("service")
}

service "api" { port = 8080 }

deployment "d1" {
    service_id = "api"      // valid: service "api" exists
}

deployment "d2" {
    service_id = "missing"  // error E076: no service "missing" found
}

@partial_requires(fields)

Declares that a partial block expects certain fields to be present after merging. This documents and enforces merge dependencies.

partial service @partial_requires(["port", "region"]) {
    env      = "production"
    replicas = 1
}

If a block that includes this partial does not provide the listed fields either directly or through another partial, a validation error is raised.

@merge_order(n)

Sets an explicit integer priority for partial merge ordering. Partials with lower n are merged first. Without this decorator, merge order follows declaration order.

partial service @merge_order(1) {
    env = "production"
}

partial service @merge_order(2) {
    env = "staging"    // this wins because it merges later
}

@child(kind, min=N, max=N, max_depth=N)

Constrains how many children of a specific kind a block may have. The kind argument is the first positional string; min, max, and max_depth are optional named integer arguments.

@child("endpoint", min=1, max=10)
@child("config", max=1)
schema "server" {
    host: string
}

Each @child entry automatically adds its kind to the schema’s allowed children set (as if it were also listed in @children). This means @child("endpoint") with no bounds is equivalent to including "endpoint" in a @children list.

Use max_depth for self-nesting blocks:

@child("menu", max_depth=3)
schema "menu" {
    label: string
}
ErrorCondition
E097Child count below minimum
E098Child count above maximum
E099Self-nesting exceeds max_depth

@tagged(field)

Names the discriminator field for tagged variant schemas. Used together with variant blocks inside the schema body.

@tagged("style")
schema "api" {
    style: string
    version: string @optional

    variant "rest" {
        base_path: string
    }

    variant "graphql" {
        schema_path: string @optional
    }
}

When a block’s tag field matches a variant’s value, that variant’s fields and containment constraints are also validated. When no variant matches, only the common fields are checked. See Schemas — Tagged Variant Schemas for full details.

@children(kinds)

Restricts which child blocks and tables may appear inside blocks of a given schema. The argument is a list of allowed block kind names and table identifiers.

@children(["endpoint", "middleware", "table:user_row"])
schema "service" {
    name: string
}

service "api" {
    endpoint health { path = "/health" }     // allowed
    middleware auth { priority = 1 }          // allowed
    table users : user_row { | "Alice" | }   // allowed (table:user_row)
    // logger { level = "info" }             // ERROR E095: not in children list
}

Special names in the children list:

EntryMeaning
"table"Allows anonymous tables (no schema ref)
"table:X"Allows tables with schema_ref = X

An empty list @children([]) forbids all child blocks and tables, making the schema a leaf:

@children([])
schema "leaf_node" {
    value: string
}

You can also constrain what appears at the document root by defining a schema named "_root":

@children(["service", "config"])
schema "_root" {}

service main { port = 8080 }     // allowed
database primary { host = "db" } // ERROR E095: not in _root children list

@parent(kinds)

Restricts where a block may appear. The argument is a list of allowed parent block kinds. Use "_root" to allow the block at the document root.

@parent(["service", "_root"])
schema "endpoint" {
    path: string
}

service "api" {
    endpoint health { path = "/health" }   // allowed: parent is "service"
}

endpoint standalone { path = "/ping" }     // allowed: parent is _root

If a block appears inside a parent not in its @parent list, error E096 is emitted.

@symbol_set(name)

Constrains a symbol-typed schema field so that only members of the named symbol set are accepted. The argument is the name of a symbol_set declaration.

symbol_set http_method {
    :GET
    :POST
    :PUT
    :DELETE
}

schema "endpoint" {
    method: symbol @symbol_set("http_method")
    path:   string
}

endpoint list_users {
    method = :GET          // valid: :GET is in http_method
    path   = "/users"
}

endpoint bad {
    method = :PATCH        // error E100: :PATCH is not in symbol_set "http_method"
    path   = "/items"
}

Use the special set name "all" to accept any symbol value without restricting to a specific set:

schema "tag" {
    kind: symbol @symbol_set("all")
}
ErrorCondition
E100Symbol value not in the declared symbol set
E101Referenced symbol set does not exist

@inline(N)

Maps the Nth positional inline argument (0-based) to a named schema field. Without @inline, inline args are collected into a synthetic _args list. With @inline, they map to proper named attributes.

schema "server" {
    port: int    @inline(0)
    env:  string @inline(1)
    host: string
}

server web 8080 "prod" {
    host = "localhost"
}
// Evaluates to: { port: 8080, env: "prod", host: "localhost" }

Any positional args not covered by an @inline mapping remain in _args. See Blocks — Inline Arguments for full details.

@text

Marks a schema field as the text content target for text block syntax. The field must be named content and have type string. A schema may have at most one @text field.

When a schema has a @text field, blocks of that type can use text block syntax — a heredoc or string literal in place of a { body }:

schema "readme" {
    content: string @text
}

// Heredoc syntax
readme my-doc <<EOF
# Hello World
This is the content of the readme.
EOF

// String literal syntax
readme short-doc "Simple one-line content"

// String interpolation works too
let name = "World"
readme greeting "Hello ${name}!"

The text content is assigned to the content field:

{
  "my-doc": { "content": "# Hello World\nThis is the content of the readme.\n" }
}
ErrorCondition
E093Block uses text block syntax but its schema has no @text field
E094@text field validation errors (wrong name or type)

@merge_strategy(strategy)

Controls how attribute conflicts are resolved during partial merges. See Partial Declarations for details.

Constraining table placement

To constrain where tables may appear, define a virtual schema with the "table" or "table:X" name:

# Tables with schema_ref "user_row" may only appear inside "data" blocks
@parent(["data"])
schema "table:user_row" {}

# Anonymous tables may only appear at the root
@parent(["_root"])
schema "table" {}

Combined constraints

Both @children and @parent are checked independently. If both are violated on the same item, both E095 and E096 are emitted. If neither decorator is present on a schema, nesting is unrestricted (backwards compatible).