---
title: "Authoring content other people can use"
description: "Everything you write here is read by somebody else — another creator, another agent, or you in a month with none of today's context. This guide is the set of rules that make what you write usable by…"
section: "Core"
slug: "core-authoring"
canonical: "https://origozero.ai/docs/core-authoring"
updated: "2026-09-05T16:41:46.320727375+00:00"
---

# Authoring content other people can use

The rules are short. There are four, and they compose.

## 1. A callable is a `typed function`

Write `typed function`, not `function`, for anything another file calls.

```lua
typed function M.damage(target: entityRef, amount: number): number
    ...
end
```

You get two things a plain `function` cannot give:

- **The argument types are enforced at runtime.** A caller passing a string
  where a number belongs is refused by name and position, at the call, instead
  of failing somewhere inside your body with a message about a value the caller
  never wrote.
- **The checker knows the signature.** A wrong-typed or wrong-arity call is an
  error before the code runs, and the editor completes the parameters.

A plain `function` is right for a module-local helper nothing outside calls.

## 2. A callable says what it is, in directives

Above the function, in `--!` directives:

```lua
--!desc Apply damage, returning the health left.
--!arg target The entity taking the damage.
--!arg amount How much to take off, clamped at zero.
--!return The target's remaining health.
--!example local hp = combat.damage(enemy, 10)
typed function M.damage(target: entityRef, amount: number): number
```

This is not decoration. It is the only thing that puts your function into
`man`, `lsp.describe`, `lsp.search` and the module's own summary. A function
with no `--!desc` is reachable only by someone already reading your source,
which means it is reachable only by someone who already knew it was there.

`--!deprecated <what to use instead>` marks a path that still works but is no
longer the one to take, and the message names the replacement.

## 3. A value is a `Field.<kind>`, and it carries its description

Component `public` data, and any other declared value, is built with a `Field`
constructor rather than a bare literal:

```lua
public = {
    speed  = Field.number(60, NoSync, "Metres per second at full throttle."),
    target = Field.entityRef(nil, Sync, "Who this turret is tracking."),
}
```

The **last** argument is the field's **description**, and it is the same kind of
obligation as `--!desc` on a function: it is what the inspector shows, what
`man` prints, and what tells the next person whether `speed` is metres per
second or a multiplier. A field with a default and no description states a
number and hides its meaning.

It is the last argument, not the third — the kinds do not all take the same
ones before it. `Field.enum(values, default, mode, …)` and
`Field.range(min, max, default, mode, …)` take their own first, and
`Field.alias(target, description)` takes no mode at all, so its description is
the second. Read the constructor in `modules/field.module` before adding one.

The trailing argument also accepts `Serialized`, or a
`{ serialized = ..., description = ... }` table when you need both.

`Field` is not only for components. An `assetType` declares its properties the
same way, and gets the same inspector and the same documentation from it.

## 4. A surface that is not a function declares its members

The three rules above cover a module that exports functions and a component
that declares data. They do not cover a **handle** — a table you hand back
whose members are reached through a metatable, where there is no `function`
statement to write a directive above and no `public` table to put a `Field` in.

`--!members <TypeName>` covers that case. It marks a table as the description
of a surface's members:

```lua
--!members InventoryRef
local MEMBERS = {
    weight = { kind = "property", type = "number",
               doc = "Total mass carried, in kilograms." },
    add    = { kind = "method", type = "(self, item: string, n: number?) -> boolean",
               doc = "Put an item in. False when it would not fit." },
    slots  = { kind = "namespace", type = "{ [string]: any }",
               doc = "The per-slot surface." },
}
```

`kind` is `property` (read as a value), `method` (called — `type` is the whole
signature, `self` first), or `namespace` (carries its own members). `doc` is the
description. `deprecated` names a replacement.

From that one table the engine takes **three** things that would otherwise be
written separately and drift apart:

- the member set the runtime accepts, so a misspelling is refused and the
  refusal lists the real names,
- the `InventoryRef` **type**, so a function returning one can say so and the
  checker knows what it has,
- each member's documentation, in `man` and `lsp.describe`.

If your handle is a plain table whose methods are ordinary functions, you do
not need this — rules 1 and 2 already describe it, and an `export type` names
it. Reach for `--!members` when the members are dispatched rather than defined.

## What this buys, concretely

A creator writing against a surface that follows these rules gets a misspelled
member refused with the correct name offered, a wrong-typed argument refused at
the call, and the ability to read what a thing does without opening the file
that defines it. A surface that follows none of them is usable only by reading
its source, and is therefore not reusable at all.

The engine's own library is written this way. When you are unsure what a rule
looks like in practice, read a neighbour: `modules/api/engine/` for typed
functions and directives, any `components/` asset for `Field`, and
`modules/api/engine/entity.module/members.module` for a member table.

## Checking your work

- `lsp.check("<path>")` on the file — the diagnostics a caller would get.
- `lsp.checkAll({ scope = "user" })` — the same across everything you authored.
- `man <your surface>` — what a reader actually sees. If it is empty or says
  less than you expected, the directives are missing, not the reader.
