Values in Every Language

Every code step does two things: it reads what the step before it produced, and it passes on a value of its own. This page shows both, language by language, with the same small example in each. It applies equally to a code cell in a CodeBook and to a Code node on a workflow canvas — they are the same step.

New to CodeBooks? Start with Your First CodeBook, which builds a notebook in Python. This page is for the other languages.

The example

Each section below reads this sheet and adds up its Total column. Make a CodeBook, add a Sheet cell and fill it in, names in the first row:

ABCD
1ItemQtyPriceTotal
2Tea23.5=B2*C2
3Cake14=B3*C3
4Scone32.5=B4*C4

Add a Code cell under it, pick the language from the cell's toolbar, paste the example, and run the sheet and then the cell (or Run All Cells). Every example passes on 18.5 (the Shell one passes it on as text).

What the cell below the sheet receives is the sheet's rows: a list of rows, the name row first. Formula cells arrive as their results — 7, not =B2*C2 — and numbers as numbers.

What every step receives

Every language gets the same thing in input: what the step above produced, with its value under value. How you write that depends on the language:

LanguageThe value of the step above
Python, Rubyinput["value"]
JavaScript, TypeScript, Luainput.value
Goinput.(map[string]any)["value"]
Rustinput.get("value")
Cjson_get(input, "value")
C++input["value"]
Zigc.get(input, "value")
Shellarrives on standard input

That holds when the step above is a code step, a sheet or an agent. A Start node with JSON data is different: its fields are in input directly — input["message"], input.message — and a webhook puts what was posted under body.

What every step passes on

LanguageThe value passed on, first match wins
Pythonreturn → the last line, when it is an expression → a variable named output, result, value or data → the input, unchanged
JavaScript, TypeScriptreturn → output, result or data → the last line, when it is an expression → the input, unchanged
Luareturn → output, result, value or data → the last line, when it is an expression → the input, unchanged
Rubythe script's own value (its last expression, or return) → output, result, value or data → the input, unchanged
Go, Rust, C, C++, Zigwhat your run function returns
Shelleverything the script prints

Printing is output, not the value

In every language, what a step prints appears under the cell (or in the node's output panel) as its printed output, and is not passed on:

LanguagePrint with
Pythonprint(...)
JavaScript, TypeScriptconsole.log(...)
Luaprint(...), io.write(...)
Rubyputs, p, print
Gofmt.Println(...)
Rustprintln!(...)
C, C++printf(...)
Zigstd.debug.print(...)

Shell is the exception: what a Shell step prints is its value.

Two habits keep this simple in every language: end with the value you mean, and don't pass on a record with a key of its own called value — the next step would take that key for the value itself.

Python

rows = input["value"]
col = rows[0].index("Total")
sum(row[col] for row in rows[1:])

The last line is the value, as in any notebook; return works too, and wins if you write it. print() is output, not the value. Python cells in a CodeBook also share their variables. The CodeBook tutorial covers pandas, pictures and more.

JavaScript

const [names, ...rows] = input.value
const col = names.indexOf("Total")
rows.reduce((sum, row) => sum + row[col], 0)

The last line is the value when it is an expression, so the reduce is what is passed on. return works too. One thing ranks above the last line: if the step sets a variable called output, result or data without declaring it (no const or let), that variable is passed on instead. A promise on the last line is awaited, so an async call passes on its result.

Each JavaScript cell starts fresh: a variable from one cell is not there in the next. Pass it on as the value instead.

TypeScript

type Row = (string | number)[]

const [names, ...rows] = input.value as Row[]
const col = names.indexOf("Total")
rows.reduce((sum: number, row) => sum + Number(row[col]), 0)

The same rules as JavaScript: types are removed before the step runs, and are not checked. See TypeScript steps for the few features that don't fit a single step.

Lua

local rows = input.value
local col
for i, name in ipairs(rows[1]) do
  if name == "Total" then col = i end
end

local total = 0
for r = 2, #rows do
  total = total + rows[r][col]
end
total

Lua counts from 1, so the name row is rows[1] and the data starts at rows[2]. The last line, total, is the value. A step that ends with a call — summarise(input) — passes on what the call returns, when that is data. output, result, value and data rank above the last line, as in JavaScript. There is no null in Lua: an empty field is simply absent. Lua values and workflow values has the rest.

Ruby

names, *rows = input["value"]
col = names.index("Total")
rows.sum { |row| row[col] }

Ruby already works this way on its own: a script's value is its last expression, or what it returns. Keys in input are strings, not symbols. The first Ruby step on a device waits for a one-time download; see Ruby, specifically.

Go

import "fmt"

func Run(input any) (any, error) {
    m, _ := input.(map[string]any)
    rows, _ := m["value"].([]any)
    if len(rows) < 2 {
        return nil, fmt.Errorf("expected a table with a name row")
    }

    names, _ := rows[0].([]any)
    col := -1
    for i, name := range names {
        if name == "Total" {
            col = i
        }
    }
    if col < 0 {
        return nil, fmt.Errorf("no Total column")
    }

    total := 0.0
    for _, r := range rows[1:] {
        row, _ := r.([]any)
        if col < len(row) {
            v, _ := row[col].(float64)
            total += v
        }
    }
    return total, nil
}

Go, Rust, C, C++ and Zig steps are compiled: you write one function, and what it returns is the value — there is no last-line rule. Numbers arrive as float64 and lists as []any; check every type assertion. Return an error to fail the step with your own message.

Rust

pub fn run(input: Json) -> Result<Json, String> {
    let rows = input
        .get("value")
        .and_then(|v| v.as_array())
        .ok_or_else(|| "expected a table".to_string())?;
    let names = rows
        .first()
        .and_then(|r| r.as_array())
        .ok_or_else(|| "expected a name row".to_string())?;
    let col = names
        .iter()
        .position(|n| n.as_str() == Some("Total"))
        .ok_or_else(|| "no Total column".to_string())?;

    let total: f64 = rows[1..]
        .iter()
        .filter_map(|row| row.at(col).and_then(|v| v.as_f64()))
        .sum();
    Ok(Json::from(total))
}

Json is built in — no crate to add. Return Ok(...) to pass a value on and Err to fail with a message; avoid unwrap() on anything from input.

C and C++

In C:

Json *run(Json *input) {
    Json *rows = json_get(input, "value");
    int n = json_length(rows);
    if (n < 2) return json_error("expected a table with a name row");

    Json *names = json_at(rows, 0);
    int col = -1;
    for (int i = 0; i < json_length(names); i++) {
        if (strcmp(json_str(json_at(names, i), ""), "Total") == 0) col = i;
    }
    if (col < 0) return json_error("no Total column");

    double total = 0;
    for (int r = 1; r < n; r++) {
        total += json_num(json_at(json_at(rows, r), col), 0);
    }
    return json_from_num(total);
}

The json_ accessors take a fallback and never crash on a missing field, so a short row simply adds nothing. Never call free.

In C++:

Json run(Json input) {
    const Json &rows = input["value"];
    if (rows.size() < 2) return Json::error("expected a table with a name row");

    const Json &names = rows[0];
    std::size_t col = names.size();
    for (std::size_t i = 0; i < names.size(); i++) {
        if (names[i].as_string("") == "Total") col = i;
    }
    if (col == names.size()) return Json::error("no Total column");

    double total = 0;
    for (std::size_t r = 1; r < rows.size(); r++) {
        total += rows[r][col].as_number(0);
    }
    return Json(total);
}

A missing key or index gives a null Json, so lookups chain safely. throw is not available — fail with Json::error(...).

Zig

pub fn run(input: Json) anyerror!Json {
    const rows = c.get(input, "value") orelse return c.stepError("expected a table");
    const names = c.at(rows, 0) orelse return c.stepError("expected a name row");

    var col: ?usize = null;
    for (0..c.len(names)) |i| {
        if (std.mem.eql(u8, c.asStr(c.at(names, i), ""), "Total")) col = i;
    }
    const total_col = col orelse return c.stepError("no Total column");

    var total: f64 = 0;
    for (1..c.len(rows)) |r| {
        const row = c.at(rows, r) orelse continue;
        total += c.asNum(c.at(row, total_col), 0);
    }
    return c.num(total);
}

std, Json and the helpers c are already in scope. c.get and c.at return optionals, which orelse unwraps. A step that doesn't read input must say _ = input;.

Shell

Shell steps run on a computer of your own, connected through Circuit. The value of the step above arrives on standard input — as text when it is text, as JSON otherwise — and everything the script prints is the value passed on. The whole record is also in $INPUT_JSON.

With a text cell or an agent above it:

tr '[:lower:]' '[:upper:]'

passes on the same text in capitals. Under the sheet, standard input is the rows as JSON, so a tool that reads JSON does the sum:

python3 -c 'import json,sys; rows=json.load(sys.stdin); c=rows[0].index("Total"); print(sum(r[c] for r in rows[1:]))'

Where next