- 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, how names are spelled, included files, loops, the pulse 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 { configField: value }
name = NodeType {}
````
`name` is the node id, unique in its scope, camelCase. `NodeType` must exist
in the project's `nodes/` catalog; you read its real ports with
`weft describe-nodes --node <Type> --compact`, never from memory.
### Spelling a name
Every name you choose in a program is camelCase: a node (`sendReply`), a
group, a loop, an `@include`, and every port you declare on a node, a group,
a loop or an included file's header (`maxRetries: Number`). On a node that takes added
inputs, a key in the braces that creates a port counts too (`userId: 3` on
`ExecPython`). A node type is spelled the way
the catalog spells it, which is PascalCase (`ExecPython`). A type you
declare is PascalCase too (`type Card = ...`), though the compiler does not
check it. Settings are spelled the way the node's interface names them,
camelCase too (`systemPrompt`).
If a name you chose is spelled any other way, the program still runs, and
the compiler warns `not-camel-case`, with the fix when it can work one out:
```text
node 'send_reply' is not camelCase: write 'sendReply'
```
A few names keep another spelling. The gates, `_should_flow` and
`_should_not_flow`, are the language's own names, so the warning skips them;
the markers keep theirs too (`@per_instance`, `@instance_filled`,
`@require_one_of`). A value you pick from a fixed list is written exactly as
the list spells it (`kind: "photo"`). And on a node whose added ports
are named after data from outside weft, a port keeps that data's spelling:
`Route` for its path's captures, `JsonObject` for the keys of the object it
builds, the Postgres query nodes for a query's `$` holes, among others.
`body = JsonObject { user_id: ask.id }` draws no warning for `user_id`; the name you give the node, `body`, is still checked.
Such a node shows `addedPortsAreDataKeys` in the `features` block of its
wiring view. A record's fields are never checked, so a body shaped
`{ user_id: String }` keeps the caller's spelling too.
## Connecting
````weft
target.inputPort = source.outputPort
````
Right to left: the value flows from `source.outputPort` into
`target.inputPort`, 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 one you write (`answer = Reply { body:
work.answer }`, never `answer = Reply` with `answer.body = work.answer` under
it). 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. Only a record type has keys to read: a source typed
`JsonDict`, a `Dict[String, String]` (a route's `query` or `headers`) or a
scalar has none (`deref-path`), and nothing reads one for you. Narrow the
source port to the shape it carries (next), or cast it to a record type
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 on the arrow where you use the node you
may write a TIGHTER one: on any input, and on an output the node marks
`narrowable` in its metadata (a query's `rows`, a model's JSON `response`, a
request's `body`), where only the program knows what shape comes out. Every
other output takes no type but its own. The compiler checks yours and refuses
one that does not fit, naming the port (`node 'ask': output port 'query' is
declared Dict[String, Number], but Route always gives it Dict[String,
String]: only an output its node marks narrowable takes the type the program
declares`), 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.
A cast (a node carrying `features.castPorts`, the catalog's `Cast` being
one) 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.
**What the outside world sends is narrowed only where the node lets you.**
A route's `query` and `headers` are not narrowable: they stay `Dict[String,
String]`, and you read a key off one with a `Cast` to an open record:
`utm = Cast -> (value: { utm: String, .. }) { value: ask.query }`, then
`utm.value.utm`. A request's `body`
is, and a record declared there judges each request as it arrives: a body
that does not fit is answered `400` naming the field, and nothing past the
route runs. A record refuses a key it does not declare, so either declare
every key the caller may send, optional ones with `?`, or end the record
with `..` (`{ email: String, .. }`), which drops the keys it does not
declare. A cast to a record is just as strict: an undeclared key fails the
cast.
A query's `row` (its first row) narrows the same way, so a lookup of one row
reads its columns straight off it, `look.row.title`:
```weft
look = PostgresExecuteQuery(id: String) -> (row: Card) {
account: db.access
query: "select title, seen from cards where id = $id"
}
```
The value is checked against the declared type when it arrives, so a shape
that fits flows and one that does not fails loudly, naming the field. 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 field with a fixed list of choices (a dropdown in the editor) takes one of
them and nothing else: any other written value is `literal-not-an-option`,
and the message lists the choices. A list the node marks `freeText` (model
ids, voices) only suggests: any value is accepted there.
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}}" }`.
How a node reads those ports (a query's parameters, a template's slots) is
in that node's description.
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 node runs,
which service is called, a call made once per item) is coordination, and it
belongs in the graph, where each node is visible and journaled: a Boolean
on `_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. Written `key?:`, the
node runs without the created port even when it is wired and the wire closes;
written without `?`, the port is required.
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
score = ExecPython -> (out: String) {
code: @file("assets/scripts/score.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. Two catalog examples:
`FirstInOrder` emits its first input that carried a value, so reordering its
lines changes which branch wins, and `List` puts its inputs into one list, in port order, and you declare the list's
type on its output. An optional port (`name?:`) that got nothing is left out.
If you want several wired values as one list (several `LlmTool`s into
`LlmInference`'s `tools`), that is `List`, never a Python node that returns
`[a, b]`:
````weft
tools = List(search: JsonDict, fetch: JsonDict) -> (list: List[JsonDict]) {
search: searchTool.tool
fetch: fetchTool.tool
}
````
Those two are the only ways line order changes what a program does.
### Literals on a connection line
````weft
post = SlackSendMessage { account: slack.access, 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: `score.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
fortyTwo = 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; on every other port a
closed input either skips its node or, written `?`, is left out, 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 `what its _should_not_flow watches did not finish
('<node>' failed: <error>)`, naming the node that broke) 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 names where it started:
`the required input 'value' closed because 'query' failed: ...`), so the rule
holds any number of skips down the line, and every skip in that chain still
names `query`, the node that broke.
The one exception is a failing node whose `_error` is wired: it handled its
failure, so its ports close as plain absences, and a `_should_not_flow` on
them runs.
### Handling a failure: the `_error` output
If you want the program to do something when a node fails (mark a row
`error` with the reason, answer the caller with a message, try another
branch), wire that node's `_error` output. Every node has one, triggers
included: weft adds it, a `String` holding why the node failed. Never
declare `_error` among a node's outputs yourself (a compile error); a plain
`error` is an ordinary name a node may use for an output of its own.
- Wired: a failure becomes its message on `_error`, the node completes, and
its other outputs close, so the success branch simply gets nothing.
- Unwired: the failure fails the run.
A bad setting, a bad input or a type error always fails the run, wired or
not: those are the program's own shape, never an outcome to route around.
So does a failure after the node already sent something on a `Bus` or a
generator output: the stream it started ends failed, since a second answer
from a branch gated on `_error` would race the first.
A group has no `_error`. If you want your own message for a group's
failure, bring the chosen nodes' `_error` out through group outputs and
answer with your own `Reply`.
What counts as a failure for a given node (an answer with an error status,
say) is in that node's description.
A node that could act outside the run is never started again after a crash. If its worker goes away mid-run, a fast run ends cancelled and a durable one fails the node that was running (How a run is kept, below, has both); only a `pure` node that had sent nothing on and read no stream runs again. With `_error` wired, that failure goes there like any other, and the log says `the worker running '<node>' went away while it was running` (or `while it was reading a stream`). A run waiting on a timer or a form, with nothing else of it running, survives its worker dying either way, unless its caller is still on the line with `outlivesCaller` off or a bus between its nodes is open: then its wait holds the worker (up to the trigger's `holdSecs`), and it ends with it.
````weft
ask = LlmInference { provider: prov.provider, prompt: job.text }
mark = PostgresExecuteQuery(reason: String, id: String) {
account: db.access
query: "update jobs set state = 'error', reason = $reason where id = $id::uuid"
reason: ask._error
id: job.id
}
````
`mark` runs only when `ask` failed: its `reason` input arrives only then.
If the reacting node needs no value from the failure, gate it instead (here on `ask` above):
````weft
fallback = Reply { body: "the backup answered", _should_flow: ask._error }
````
This is how a program records a failure. Never catch it inside Python.
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
Auf GitHub ansehen