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

Weft The book

Reading inputs, emitting outputs

What the methods are is on the ctx. This page is what happens around them: how a value gets into your bag, and what the runtime does with what you send.

Where an input’s value comes from

Wired data and design-time settings live in one bag, under the port’s name. Three things can fill a port, in this order:

RankSource
1A value that arrived on a wire
2A value written into the node’s body, if nothing arrived
3The default from the node’s metadata, if the port is still empty

Four rules sit around that.

A closed wire kills the default. If a wire closes without carrying a value (weft calls that a closure: upstream finished without sending on that port, or failed), the default does not fill in, because that would hide that something did not happen.

A delivered null is a value only where the type allows it. On a port whose type admits Null, null is data. Anywhere else, a null arriving on a wire fails the run, unless the program marked that input optional (x?: T where it uses your node): then it reads as nothing arrived, and the default fills.

A wrong type fails the run, required or not, and the program cannot catch it (which failures a program can handle). It is never dropped so the default can take over.

A number outside its number widget’s min and max fails the run too (widget), and so does a fraction where the widget’s step is whole: a poll interval of 0 on a port with "min": 1 fails rather than becoming the port’s default. So does a value that is not one of a select widget’s options, unless the widget sets freeText.

An input your metadata leaves optional can be empty when your body runs: nobody wired it and it has no default, or the program wrote ? on it and its wire closed. Read such an input with opt or get_or (reading inputs). Whether your node runs at all then is up to the program. For how a program decides, go and read optional and required inputs.

Two ports get special handling before your code sees them. A connection input arrives as a full Access value carrying the service, rather than the id the editor stored. And the gate ports are stripped out entirely, so your node never finds a _should_flow it did not declare.

A trigger is different again: on a real firing, its ports replay the values that were frozen when the trigger was set up, and written literals fill whatever is left.

Emitting

ctx.pulse_downstream is the only way out.

Once per port, per firing. Emitting or closing the same port twice is an error naming it:

node 'reply' touched port 'text' twice in one firing. Each output port can be
emitted or closed AT MOST ONCE per firing

If your node emits, pauses on a wait, and its body runs again from the top when the answer comes, the emit made again does not count as a second one: it does nothing, and the first value stands. For why the body runs again, go and read surviving a restart.

Several calls are fine if they touch different ports. That is how you release a bus marker early and a done flag at the end. If any port in one call collides, nothing in that call is recorded, so a failed call is a clean no-op rather than half an emission.

A stream port is the exception. A Generator[T] output takes as many emissions as you like, one per item, until you close it. After the close, nothing can follow.

Anything you never mention is closed for you when the body returns. That closure is the signal everything downstream is waiting for. A node that decides it has nothing to say does not need to do anything special: it just returns, and the branch behind it skips.

A node that fails closes whatever it had not already sent. What it did send stays sent. weft cannot unsend a message your node already put on Slack.

An undeclared port is caught first, before the once-only rule, so a typo reads as the real problem:

node 'reply' tried to emit on undeclared output port 'txet'. Declare it in
metadata.json's outputs list, or correct the port name in the node body.

A value that does not fit its port’s type fails the call, naming the port, the type and what did not fit. The type is the one in your metadata, after the program filled in any type variable. If your metadata marks the output narrowable (outputs), it is the type the program declared on it, like -> (response: Verdict). If the type is an open record, extra keys are dropped instead of failing the call (records that take extra keys). A refused call sends nothing and leaves the port open, so if your code handles the Err instead of returning it with ?, it can emit on that port again with something else.

A value over 100 KB fails the call, naming the port. The weight is the value written as JSON, checked on what you emit, so a node one hop later cannot trim it for you. There are two ways out, and the refusal names both: put the data in storage and send the stored file, whose value weighs a few hundred bytes whatever the file holds, or send less. Sending less means sending one page of it along with where the next page starts, or cutting it short and saying so in another output.

If you want to bound a value yourself, weft::storage measures it the way the wire does, against MAX_WIRE_VALUE_BYTES:

You wantCall
What a value weighs on a wirewire_weight(&value)
The longest start of a text that weighs at most budget, never splitting a charactercut_to_weight(text, budget)
A text cut to max_chars characters, then to what one port carries, and whether anything was cutcap_text(text, max_chars)

FetchPage uses cap_text this way: a maxChars input, and a truncated output that tells the program the page was cut.

Waiting for the value to be taken

pulse_downstream sends and carries on. yield_downstream waits until the value was actually taken, which is what you want when you are producing faster than the consumer reads.

It fails rather than waiting forever when delivery becomes impossible, for instance because the consumer skipped or the run finished. An unwired port counts as delivered immediately.

Building the output

NodeOutput::new()
    .set("count", words.len() as f64)
    .set("longest", longest)

set takes anything that turns into JSON. If you already have a Value, it passes through rather than being wrapped again.

For a dynamic object whose keys should land on same-named ports, ctx.fan_declared(&value) matches them against the ports your node actually declares and skips the rest. That matters after a paid call: an extra key in a provider’s response should not fail your node when you already spent the money.

NodeOutput::stored_file(stored) fills the four ports a stored file travels as: file, filename, mimeType and sizeBytes.

Errors

You wantWrite
To wrap somebody else’s errorresult.node_err("reading the reply")?
To say what was missing when an Option was Noneoption.node_err("the response had no body")?
To fail on something you worked outweft::node_bail!("pick a channel or a user")
The same, as an expressionnode_error(format!("..."))

Those, plus ? on anything the ctx returns, cover every failure except a mistake in the program itself, which you return as Err(WeftError::Input(..)) (see which failures a program can handle).

Write the message as an instruction to whoever is building the program, because that is who reads it. “pick a destination: a channel or a user” beats “invalid configuration”.

Which failures a program can handle

Every node has an _error output, which weft adds when it loads the node. If a program wires it, your node’s failure comes out there and the node counts as done; if nobody wired it, the failure fails the run. For what that looks like from the program, go and read when a node fails. Your body writes no handling for it: it returns its errors with ? as usual.

_error is weft’s, so a body that emits on it or closes it fails. error, without the underscore, is yours to use like any other name.

What your node does decide is which kind of failure it returns. A failure of the work itself can be caught on _error: anything from node_err or node_bail!, a value your node emits that does not fit its port (like a model’s answer in the wrong shape), and a wait that gave up (go and read when the run cannot pause). A mistake in the program (a bad setting, or a value of the wrong type arriving on an input) always fails the run, even when the program wired _error, because the fix is in the program. If your node asks ctx.output_type about a port it does not declare, that fails the run too, and _error never sees it.

So when your node spots a mistake in the program itself (SQL that does not parse or names a table that is not there, an input value of the wrong shape, an empty field it needs), fail with Err(WeftError::Input(..)). If a program could sensibly retry or route around the failure while it runs, use node_bail! instead. If your node talks to a server, decide the same way for each kind of refusal: the Postgres nodes fail the run on a syntax error or a missing table, and put a broken constraint on _error.

If your node produces a stream, its failure stops reaching _error once something went out on a Bus or Generator output. So do the work that can fail before you send the first piece. For a bus, ctx.pending_bus waits to open it until you have the first piece (go and read streams and buses in Rust).

In a test, rig.wire_output("_error") wires _error the way a program would, and outcome.output("_error") reads what came out. Without it the rig wires nothing, so the same node fails loudly.