Schema (S) reference
The S builder, value types, validation, and Infer.
Workflow input is described with the global S builder. S is available
without an import; each method returns a plain JSON-serializable descriptor. The
runtime validates the caller's JSON input against the schema (after filling
defaults) before main runs, and persists the input for deterministic replay.
Type definitions live in
runtime/src/tide.d.tsfor editor autocompletion.
The workflow convention
Every workflow export defaults { schema, main }:
const schema = S.object({ name: S.text() });
export default {
schema,
main: async (ctx: Infer<typeof schema>) => {
log(ctx.name);
},
};defineWorkflow({ input, run }) is sugar over the same shape — see the
Workflow API.
Value types
Tide's value types map to Argon's scalar types. Each builder returns a
descriptor with a type (and a valueType):
| Builder | Accepts (input JSON) | Infer type |
|---|---|---|
S.text() | a string | string |
S.integer() / S.int() | an integer | number |
S.nat() | a non-negative integer | number |
S.decimal() | an integer/decimal as number or numeric string | string | number |
S.real() | a number or numeric string | string | number |
S.money() | { amount, currency } (currency is an ISO 3-letter code) | { amount, currency } |
S.date() | an ISO date string YYYY-MM-DD | string |
S.dateTime() | an ISO datetime string | string |
S.boolean() | true / false | boolean |
S.iri() | a non-empty string | string |
S.conceptRef() | a non-negative integer id | number |
S.individualRef() | a non-negative integer id | number |
S.text() // { type: "text", valueType: "Text" }
S.integer() // { type: "integer", valueType: "Integer" }
S.money() // { type: "money", valueType: "Money" }Composition
Objects
S.object({
name: S.text(),
age: S.integer(),
bio: S.optional(S.text()),
})S.object automatically tracks which fields are required: a field is required
unless it is wrapped in S.optional(...) or carries an S.default(...).
Lists and sets
S.list(S.text()) // an ordered array of strings
S.set(S.integer()) // a set of integers (an array on the wire)
S.list(S.object({ ... })) // a list of objectsOptional and nullable
S.optional(S.text()) // the field may be omitted (string | undefined)
S.nullable(S.text()) // the field may be null (string | null)
S.optional(S.nullable(S.text())) // string | null | undefinedoptional means the field may be absent from the input. nullable means it
may be present as null.
Defaults
S.default(S.integer(), 3) // omit it → arrives as 3
S.default(S.enum(["low", "medium", "high"]), "medium")A defaulted field may be omitted on input; the default is filled in before
validation, so main always sees a value. (Defaulted object fields are
therefore always present in the Infer type, unlike plain optional fields.)
Literals and enums
S.literal("active") // exactly "active"
S.literal(42) // exactly 42
S.enum(["pending", "active", "done"]) // one of the listed values
S.enum([1, 2, 3]) // one of the listed numbersUnions
S.union([S.text(), S.integer()]) // string | number
S.union([
S.object({ kind: S.literal("a"), value: S.text() }),
S.object({ kind: S.literal("b"), value: S.integer() }),
])A value validates if it matches at least one variant.
Nested composition
Schemas compose freely:
const address = S.object({
street: S.text(),
city: S.text(),
zip: S.text(),
});
const schema = S.object({
name: S.text(),
address,
shippingAddress: S.optional(address),
});Type inference with Infer
Infer<T> is a global compile-time type that maps a schema descriptor to its
TypeScript type — full editor autocompletion, zero runtime cost.
const schema = S.object({
name: S.text(),
count: S.integer(),
priority: S.default(S.enum(["low", "medium", "high"]), "medium"),
tags: S.optional(S.list(S.text())),
config: S.object({
retries: S.default(S.integer(), 3),
verbose: S.optional(S.boolean()),
}),
});
// Infer<typeof schema> resolves to:
// {
// name: string;
// count: number;
// priority: "low" | "medium" | "high"; // defaulted → always present
// tags?: string[]; // optional → may be undefined
// config: {
// retries: number; // defaulted → always present
// verbose?: boolean;
// };
// }Validation
Validation runs in JavaScript before main, after defaults are applied. It
walks the descriptor and collects every error with a JSON-path to the offending
field. A failing run reports all errors at once:
Input validation failed:
$.name: expected Text string, got number 123
$.count: required but missing
$.config: required but missingWhat is checked:
- Value type per the table above (
text,integer,nat,decimal,real,money,date,datetime,boolean,iri,conceptRef,individualRef). - Required fields — every non-optional, non-defaulted object property must be present.
- Unknown keys — extra object properties are rejected.
- Literal / enum — exact match against the allowed value(s).
- List / set items — each element validated against the item schema.
- Union — at least one variant matches.
- Optional / nullable — missing/
undefinedallowed for optional;nullallowed for nullable.
Empty schemas
A workflow that needs no input uses an empty object:
const schema = S.object({});
export default {
schema,
main: async (_ctx: Infer<typeof schema>) => {
log("no input needed");
},
};tide run workflow.ts # no --input flag neededInput persistence
Input is persisted per invocation for deterministic replay:
- First run — the input is saved alongside the journal and timestamp.
- Replay — the stored input is loaded automatically; you do not pass
--inputagain. - Mismatch — passing a different
--inputon replay prints a warning and uses the stored input.
Provide input with --input '<json>' or --input-file <path> (mutually
exclusive). See the CLI reference.
Related
- Workflow API —
defineWorkflow,step,hitl, the VFS. - CLI reference —
tide schema,--input. - Your first workflow — a schema in context.