- name
- weft-language
- description
- Read before writing or editing any .weft source: declarations, wiring, config literals, reserved keys, inline signatures and expressions, types, groups and the level rule, included files, loops, the pulse execution model, @file, @asset and @include, and every compiler error slug.
# The weft language
One way to say each thing, and the compiler refuses everything else. This
page is the whole surface as of this template; when it disagrees with your
memory, this page wins.
Two words the whole page leans on. A [pulse] is one value emitted on one
output port and addressed to one input port of one node; nothing walks the
graph, a node fires when every required input holds a [pulse]. A [closed]
pulse carries no value and means "nothing will ever arrive here"; a port
that never receives anything is closed the same way.
## Declaring a node
````weft
name = NodeType
name = NodeType { config_field: value }
name = NodeType {}
````
`name` is the node id, unique in its scope, snake_case. `NodeType` must exist
in the project's `nodes/` catalog; you read its real ports with
`weft describe-nodes --node <Type> --compact`, never from memory.
## Connecting
````weft
target.input_port = source.output_port
````
Right to left: the value flows from `source.output_port` into
`target.input_port`, and the types must be compatible. Every required input
gets a wire or a literal; an optional one (`port?`) can stay unwired. This
line is the longhand; [the shorthand] (next section) is the same wire inside
the target's braces, and the longhand survives only where the language
leaves no choice: a [boundary port], one of the ports a group, a loop, or an
`@include` alias declares in its signature.
If you want one key of a record value, keep going with dots
(`wpm: reader.profile.stats.wpm` in the braces, or
`speed.wpm = reader.profile.stats.wpm`). Still one wire; the compiler checks
every key against the record type at that level and the wire carries the
last key's type. A source typed `JsonDict` or a scalar has no keys to read
(`deref-path`): narrow the source port to the shape it carries (next), or
`Cast` first. At run time a `?` key found absent (or `null`) closes that
wire alone; a required key absent fails the firing.
### Narrowing a port to the shape it carries
A port's type is a contract, and you may write a TIGHTER one on the arrow
where you use the node. The compiler checks yours against the node's own and
refuses a contradiction (`declared type ... incompatible with catalog type`),
so a narrowing that compiles is one the node can honour. This is how a loose
container becomes a shape you can read keys off, and it costs nothing at run
time: the value is already that shape, the port now says so.
A port the node already declares takes the tighter type the same way one you
add on the arrow does. Here the node's own `rows` port is `List[JsonDict]`,
which has no keys to read, and naming the row's shape makes `.title` legal:
````weft
type Card = { title: String, seen: Number }
rows = PostgresExecuteQuery -> (rows: List[Card]) {
account: store.access
query: "select title, seen from cards"
}
````
A column that can be NULL is declared so in the shape: `{ title: String,
extra: JsonDict | Null }`. The declared type judges every row at run time,
and a `JsonDict` alone refuses a null, so a day when every row has the
column empty fails the node with `emitted a value on port 'rows' that its
declared type 'List[Card]' does not accept (got List[Dict[String, String |
Null]])`; the fix is the `| Null` on that field, never a looser row type.
`Cast` is for a value that has to CHANGE type while it travels. A value that
already fits needs no node at all, only a port that names the type. If you
catch yourself adding a `Cast` to put a shape on a value that already has
it, stop and write: "Wait. Narrow the port." Then write the type on the
arrow and delete the `Cast`.
**Narrow only what you control the shape of.** A port fed by the outside
world (a query string, request headers) holds whatever the caller sent, and
a record refuses a key it does not declare, at run time, on the real
request. Narrowing those compiles and then fails the first time somebody
appends a tracking parameter. Leave them as the loose type the node declares
and read the one key you want with a node.
When the opaque value comes from a node that lets you ADD OUTPUT PORTS, name
the pieces you want on the arrow instead. Each arrives on its own port under
the type you wrote, so there is nothing to unpack and nothing to Cast:
```weft
look = PostgresExecuteQuery(id: String) -> (title: String, seen: Number) {
query: "select title, seen from cards where id = $id"
}
```
Declare the type you actually want there, a named one included
(`-> (card: Card)`): the value is checked against it when it arrives, so a
shape that fits flows and one that does not fails loudly, naming the field.
Reach for the record type when the source cannot name the pieces for you, and
prefer a NAMED type over an inline one once more than one node speaks it:
the name is the contract, and it is written once.
## Config values
Typed JSON-ish literals:
````weft
t = Text { value: "a string" }
n = Range { to: 10, step: 2 }
flag = SomeNode { enabled: true }
arr = SomeNode { items: [1, 2, 3] }
obj = SomeNode { opts: { "k": "v" } }
````
Commas between fields are optional; a field per line with no commas reads
the same. `null` is never a literal: omit the field instead
(`config-null-literal`).
A list or object literal holds plain values only, so `params: [self.chatId,
self.pushName]` is refused. When a node needs several values from the graph,
each one is its own input port. When a node accepts however many values you
give it, declare them in its inline signature:
`PostgresExecuteQuery(chat_id: String, push_name: String) { ... }` and the
SQL reads `$chat_id`; `Format(user: String) { template: "Hi {{user}}" }`.
A Python node whose code has no branch, no loop and no call is moving
values, not deciding anything, and the language moves values. Read the code
you are about to write and apply that test to it. Pulling a field out of a
dict is the case you will meet most; the test is what catches the rest.
If you catch yourself writing one, stop and write: "Wait. That is
plumbing." Then read the key with dots, narrow the port to the shape it
carries, declare the ports on the consumer, or wire `_should_flow` (under
Reserved keys).
Calling a library, parsing what no type describes, reshaping a list in
memory: that is processing, and Python is the right answer for it. A branch
or a loop that decides what the program does next (whether a step runs,
which service is called, a call made once per item) is coordination, and it
belongs in the graph, where each step is visible and journaled: a `Switch`
with `_should_flow` for the branch, a `Loop` for the repetition, one node
per call.
Building an object out of values you already hold (a reply body, a payload)
is the case you will meet most, and it has its own node: wire a value onto
each key you want and the object comes out keyed by those names. An object
LITERAL cannot hold a wire, which is what used to force a script here.
### Wires in the braces
A field whose value is `source.port` is a wire, written inside the node it
feeds. This is [the shorthand]: a node's wires sit next to its settings:
````weft
reply = TelegramSendMedia {
account: telegram.access
kind: "photo"
chatId: ask.chatId # same as `reply.chatId = ask.chatId`
file: picture.image
}
````
A `Group` is the exception: its braces hold its children, so the only field it
reads there is `_should_flow`, and its [boundary port]s are wired from
outside on their own lines.
### A key that CREATES a port
Some node types accept extra inputs beyond the ones they declare, and on
those a key naming no declared port creates one. A wire types it from the
source, a literal from its own type, `null` is an error. `key?:` makes the
created port optional, whatever the node's own default is.
Which types accept them is the node's to say, not this page's: its wiring
view carries a `features` block, and `canAddInputPorts` there means you may
add inputs, `canAddOutputPorts` that you may name outputs on the arrow. A
node with neither key, or no `features` block at all, takes only the ports
it declares.
````weft
step = ExecPython -> (out: String) {
code: @file("assets/scripts/step.py")
text: draft.answer # a String port, from the wire
limit: 3 # a Number port, from the literal
notes?: review.notes # optional: a closed pulse here does not skip the node
}
````
Created ports keep written order, and whether a node reads that order is the
node's own business, stated in its description. `FirstInOrder` is the
catalog node that does: its first input that carried a value is the one it
emits, so reordering its lines changes which branch wins. That is the only
way line order changes what a program does.
### Literals on a connection line
````weft
post = SlackSendMessage { channel: "#alerts" }
post.text = "deploy finished"
````
Both spellings are one constant, and no port takes one spelling and refuses
the other. What a port can refuse is a family:
`literal` (a value written in the source, markers included) or `wire` (a
value another node produces), through `accepts` in its metadata; absent means
both. Wrong family is `input-accepts`, and the message reads the list back
("`params` accepts: wire"). A compiler-read port (a form's `fields`, a
switch's `cases`, the access picker) takes an inline typed value only: no
wire, no `@file`, no `@asset`. The line also works on a [boundary port]. An
output port never takes a value, on any node: `step.out = "lit"` and
`out: "lit"` beside a `-> (out: String)` signature are refused, and so is a
group's own output written from inside (`self.result = "lit"`). A firing
emits on an output; you read it as `node.port`.
### Multi-line strings
````weft
step = ExecPython() -> (out: Number) {
code: ```
return {'out': 42}
```
}
````
The opening fence goes on the key's line, content starts on the next line,
the closing fence on its own line.
## Reserved keys
Exactly four, all starting with `_` (any other `_` key is an error):
| Key | Effect |
|---|---|
| `_label: "..."` | display label. A string, set once, never by wire |
| `_tags: ["a", "b"]` | tags, used by signal scoping |
| `_should_flow: <wire or false>` | decides whether this node runs at all |
| `_should_not_flow: <wire>` | the same decision read backwards: runs when what is wired here did NOT arrive |
`_should_flow`: leave it out and the node runs. Wire it and the node runs
only when what arrives is not `false`; a `false` or a [closed] pulse
(whatever decides never spoke) skips the node, closing its outputs, skipping
everything behind it. `_should_flow: false` in the braces turns one node off.
Groups and Loops take it too, inside the braces or on the container's name
from outside.
`_should_not_flow` is the mirror image: wire something into it and the node
runs when that thing closes instead of when it arrives. It is the only port
in the language that can start a node on a closure; every other port does the
opposite, skipping its node once an input closes, which is why running on an
absence needs a dedicated spelling.
Reach for it when the absence is data: a key the caller never sent, an
optional input nobody filled in. When the absence is a decision a node of
yours already made, put that decision on a second output port and gate on it
with `_should_flow` instead; the wire then reads forwards, cause before
effect.
A FAILURE is not an absence. When the node it watches fails, its ports close
too, but that closure carries the error, and the gate reads it: the node
stays off (skipped with `the node its _should_not_flow watches did not finish
(...)`, the error in the brackets) and the run reports the failure. So a
route's "no rows, answer 404" branch never fires over a database that is
down; the caller gets the failure instead. A group's or a loop's outputs
close the same way when something inside failed, so the rule holds one scope
up, and a node that SKIPPED because its input closed on a failure closes its
own ports with that failure too (its skip reason ends in `: a node before it
failed (...)`), so the rule holds any number of skips down the line.
A node carries one gate, never both: wiring `_should_flow` and
`_should_not_flow` on the same node is a compile error, `two-gates`. The
editor draws both as the same triangle, with a small circle marking
`_should_not_flow`; right-click the gate to flip which one it is.
`_should_flow` is also the ordering wire, and it accepts ANY port: any type,
any node, no declaration needed on either side. `typed = ExecPython {
_should_flow: typing.done }` means run once `typing` has fired. The value is
never read, so you never invent a port to carry it (an `after: typing.done`
port the code ignores puts a value on the graph that nothing reads, while
`_should_flow` already shows in the graph as a permission wire). The one
value that IS read is `false`, so wire a Boolean port here only when its
`false` should mean "do not run".
That is also how a node with nothing to receive gets its turn. `Close` takes
no data at all, so ending a branch early is one wire from any port on it:
````weft
bye = Close { status: 204 }
bye._should_flow = cleanup.removed
````
A branch that skipped closes its ports, so whatever hangs off it by
`_should_flow` skips with it: the branch that did not run does not act.
On a group or an included file the wire means "run what is in here": a
route's run takes the whole group along, and everything the group needs
(a database wired into it from outside), exactly as it takes a node.
````weft
live = Route { path: "live/count", method: "GET" }
work = Group(db: Access) {
rows = PostgresExecuteQuery { account: self.db, query: "select count(*) from cards" }
out = Reply
out.body = rows.rows
}
work.db = db.access
work._should_flow = live.method # the route runs the group; nothing else connects them
````
### Running something once, when the program goes live
If you want something to happen once before a program serves anything
(creating tables, seeding a row, warming a cache), write the node and wire it
into the trigger. That is the whole mechanism, and it is worth understanding
rather than memorising.
Everything UPSTREAM of a trigger is the trigger's setup program, and weft
runs that program once, at activation. On a fire the trigger reads none of
it: a fired trigger's inputs come from its bake, so nothing upstream runs
again.
Any wire into the trigger puts the producer there, not only the gate. The
gate is just the wire with nothing else to say:
````weft
make = PostgresExecuteQuery { account: db.access, query: @file("assets/sql/schema.sql") }
live._should_flow = make.count
````
Whatever that setup does, it does again on every activation, so it has to be
safe to do twice: doing it a second time must not undo, duplicate or refuse
what the first time did. How you write that is the node's business, and the
node's own description says how; the language's part is only that it will
happen more than once.
The same holds for infrastructure: anything upstream of an infra node is that
node's setup program and runs when infra starts.
Two things follow that surprise people. A `weft run --fire` warns that the
wire was not delivered, which is correct and not a problem: that wire's whole
job was to run at activation. And when the trigger lives in an included file
and the setup node does not, the value has to cross the include's boundary
like any other, so the file declares a port for it; there is no shortcut, and
keeping the setup node in the same file as its trigger avoids the crossing
entirely.
That second one has a limit: setup that several triggers SHARE (the schema
every included file's tables live in) is written once, in the file that
includes them, and each included file takes its result as a port. Every
trigger's setup runs at activation as one program, and two nodes that do not
depend on each other run at the same time, so two files each carrying their
own `create extension if not exists pgcrypto` race each other and one of
them fails on a duplicate key. "Safe to do twice" is not "safe to do twice
at once".
## Inline port signatures
Types that leave ports open declare them in the declaration:
````weft
calc = ExecPython(a: Number, b: Number) -> (sum: Number, diff: Number) {
code: "return {'sum': a + b, 'diff': a - b}"
}
answer = LlmInference -> (response: String)
ok = Cast -> (value: Boolean)
````
Inputs arrive in Python as variables named after ports; the code returns a
dict keyed by output port name; `None` or a missing key emits no [pulse] on
that port. Write only the ports the type leaves open (a `MustOverride`
output must be pinned once something reads it; one nothing reads can stay
unpinned). An empty body equals no body.
## Inline expressions
A node literal as a value, with a mandatory trailing `.port`:
````weft
out.data = Text { value: "hi" }.value
provider: OpenRouterProvider { model: "z-ai/glm-5.3" }.provider
````
It synthesizes an anonymous child with id `{host}__{field}` plus the edge.
Full node syntax nests inside, including its own signature. Omitting `.port`
is an error (ambiguity). Bare form `Type.port` takes default config.
## Comments and descriptions
`#` starts a line comment. If the first line inside a group or loop body is a
plain comment, it becomes that container's description, shown when collapsed.
Keep it one line, saying what the container does for its caller. Project name
and id live in `weft.toml`, never in a source header.
## Directives
````weft
lookup = SlackFindUser {
@require_one_of(email, id)
}
````
`@require_one_of(a, b)` on its own line in a node or inline signature (a
group or loop refuses it; put it on the node inside that needs the ports):
at least one named input must be satisfied. Compile error when unmet, and a
compile error when a name is not a port of that node (on a node that takes
custom ports, a name is a port once the header declares it, a wire lands on
it, or a config key names it); at run time the node skips when every port in
the group arrives [closed].
`@per_member` on its own line in an infra node (no arguments; anything else is
`per-member-ineligible`) gives each member of the program their own copy of
that node. `@member_filled` (or `@member_filled(<fallback>)`) where a field's
value goes makes that value each member's own, a connection included
(`account: @member_filled`); the fallback may be a value, `@file(...)` or
`@asset(...)`; never on a wired field (`member-filled-wired`) or
a group's own port (`member-filled-boundary`). Every node reading either
follows, per member. The `weft-members` skill has what that means and the shape
a program with members takes.
## Types
| Kind | Written |
|---|---|
| primitives | `String`, `Number`, `Boolean`, `Null` |
| stored files | `Image`, `Video`, `Audio`, `Blob` (references, not bytes) |
| aliases | `Media` = Image \| Video \| Audio; `File` = Media \| Blob |
| containers | `List[Number]`, `List[List[String]]`, `Dict[String, String]` |
| unions | `String \| Number`, `Number \| Null` |
| records | `{ role: String, name?: String }`, strict: undeclared keys refused |
Ver en GitHub