Skip to main content

Stage 3 · Seven languages

Code and Code file blocks accept seven language values. Every lesson in this stage builds the same block: it reads trigger.text and publishes it as message. That way you can compare the languages side by side.

Starter workflow

Paste this once, then swap in each lesson’s language and body:

schema: 1
name: Seven languages
id: seven-languages
blocks:
  - id: echo
    name: Echo
    kind: code
    language: python
    uses: [trigger.text]
    input_sample: |-
      {"trigger":{"text":"Hello from curl"}}
    body: |
      # replace with the lesson's source
    outputs:
      type: object
      properties:
        message:
          type: string

Test each one with Open code editor → Execute step.

At a glance

LanguagelanguageRunnerJSON in the standard library?
BashbashbashNo. Use jq
PowerShellpowershellpwsh -NoProfile -NonInteractive -ExecutionPolicy BypassYes (ConvertFrom-Json)
Pythonpythonpython3 -uYes (json)
C#csharpdotnet run snippet.cs (.NET 10+)Yes (System.Text.Json)
Rustrustrustc --edition 2024 -ONo, and there’s no Cargo
JavaScriptjavascriptnode snippet.mjs (ES module)Yes (JSON)
TypeScripttypescriptNode 22.6+ native, or tsxYes (JSON)
Read trigger.text → publish message
set -euo pipefail
jq -ce '{message: .trigger.text}' "$TERMORQ_INPUT"
$data = Get-Content -Raw -LiteralPath $env:TERMORQ_INPUT | ConvertFrom-Json
@{ message = $data.trigger.text } | ConvertTo-Json -Compress
import json, os
data = json.load(open(os.environ["TERMORQ_INPUT"], encoding="utf-8"))
print(json.dumps({"message": data["trigger"]["text"]}))
using System.Text.Json;
using System.Text.Json.Nodes;
using var doc = JsonDocument.Parse(File.ReadAllText(Environment.GetEnvironmentVariable("TERMORQ_INPUT")!));
var text = doc.RootElement.GetProperty("trigger").GetProperty("text").GetString();
Console.WriteLine(new JsonObject { ["message"] = text }.ToJsonString());
use std::{env, fs};
fn main() {
    let input = fs::read(env::var("TERMORQ_INPUT").unwrap()).unwrap();
    println!("{{\"input_bytes\":{}}}", input.len()); // declare input_bytes: number
}
import { readFileSync } from "node:fs";
const data = JSON.parse(readFileSync(process.env.TERMORQ_INPUT, "utf8"));
console.log(JSON.stringify({ message: data.trigger.text }));
import { readFileSync } from "node:fs";
type Input = { trigger: { text: string } };
const data = JSON.parse(readFileSync(process.env.TERMORQ_INPUT!, "utf8")) as Input;
console.log(JSON.stringify({ message: data.trigger.text }));

The short versions above leave out validation. Each lesson shows the production version, which validates its input.

  • Bash

    jq-based JSON handling, strict mode, and returning compact output.

  • PowerShell

    Raw reads, ConvertFrom-Json, and compact ConvertTo-Json output.

  • Python

    Standard-library JSON, stderr diagnostics, and Unicode-safe output.

  • C#

    .NET 10 file-based apps, System.Text.Json, and reflection-free output.

  • Rust

    rustc without Cargo: standard library only, byte-level input, and careful output.

  • JavaScript

    Node.js ES modules, node: built-ins, and JSON.stringify output.

  • TypeScript

    Native Node type stripping, erasable syntax, and runtime validation.

Subsections of 3 · Seven languages

Bash

Goal

Write a Bash block that validates its input and publishes JSON safely. Time: 10 minutes. Needs: bash and jq on PATH.

language: bash
set -euo pipefail
: "${TERMORQ_INPUT:?TERMORQ_INPUT is missing}"
jq -ce '
  if (.trigger.text | type) == "string" then
    {message: .trigger.text}
  else
    error("trigger.text must be a string")
  end
' "$TERMORQ_INPUT"

Things to know

  • Workflow Bash is always Bash, even if your terminal tab runs zsh or fish. On Windows, install Git Bash or expose WSL’s bash on PATH.
  • set -euo pipefail together with jq -e makes parse and validation errors fail the block.
  • Build output with jq -n --arg or --argjson. Never build JSON with echo "{\"message\":\"$x\"}", because quotes or newlines in $x will break it.
  • .sh files, and files with unrecognised extensions, default to Bash in Code file.
  • ✦ Autogenerate writes Bash that reads each value with jq.
Check your run

Execute step with the starter sample prints {"message":"Hello from curl"}. Step input {"trigger":{"text":42}} fails with trigger.text must be a string.

Exercise

Publish words (number): the number of whitespace-separated words in trigger.text.

Details
set -euo pipefail
text=$(jq -r '.trigger.text' "$TERMORQ_INPUT")
words=$(wc -w <<<"$text" | tr -d ' ')
jq -nc --argjson words "$words" '{words: $words}'

Declare words: { type: number }.

PowerShell

Goal

Write a PowerShell block that reads nested JSON and publishes one compact line. Time: 10 minutes. Needs: pwsh.

language: powershell
$ErrorActionPreference = 'Stop'
if ([string]::IsNullOrWhiteSpace($env:TERMORQ_INPUT)) {
    throw 'TERMORQ_INPUT is missing'
}
$data = Get-Content -Raw -LiteralPath $env:TERMORQ_INPUT | ConvertFrom-Json
$text = $data.trigger.text
if ($text -isnot [string]) {
    throw 'trigger.text must be a string'
}
@{ message = $text } | ConvertTo-Json -Compress -Depth 10

Things to know

  • Files run with -NoProfile -NonInteractive -ExecutionPolicy Bypass, so your profile isn’t loaded.
  • -Raw reads the whole file. -LiteralPath stops PowerShell treating [ and * in the path as wildcards.
  • Always use -Compress. Without it, ConvertTo-Json prints several lines and breaks the last-line rule. Add -Depth for nested objects.
  • $ErrorActionPreference = 'Stop' turns non-terminating errors into failures.
  • Captured Command Prompt commands become PowerShell blocks that call cmd.exe through %ComSpec%.
Check your run

Execute step prints {"message":"Hello from curl"}.

Exercise

Publish upper (the text in upper case) and length (number) in the same object.

Details
$data = Get-Content -Raw -LiteralPath $env:TERMORQ_INPUT | ConvertFrom-Json
$t = [string]$data.trigger.text
@{ upper = $t.ToUpperInvariant(); length = $t.Length } | ConvertTo-Json -Compress

Python

Goal

Write the standard Python block, which most later lessons use. Time: 5 minutes. Needs: Python 3.

language: python
import json
import os

with open(os.environ["TERMORQ_INPUT"], encoding="utf-8") as stream:
    data = json.load(stream)
text = data["trigger"]["text"]
if not isinstance(text, str):
    raise ValueError("trigger.text must be a string")
print(json.dumps({"message": text}, ensure_ascii=False))

Things to know

  • The runner looks for python3, then python, and runs it unbuffered (-u). Print statements appear in order.
  • An uncaught exception exits with a non-zero code, so the block fails and the traceback appears in the error output.
  • ensure_ascii=False keeps Unicode readable. The output is valid JSON either way.
  • Use print(..., file=sys.stderr) for diagnostics.
  • Only the standard library is guaranteed. If you pip install something, it’s installed for that interpreter on this computer, not for the workflow.
Check your run

Execute step with {"trigger":{"text":"Grüße 👋"}} publishes message: "Grüße 👋".

Exercise

Publish slug: the text in lower case, with runs of non-alphanumeric characters replaced by -, and leading and trailing - removed.

Details
import json, os, re
text = json.load(open(os.environ["TERMORQ_INPUT"], encoding="utf-8"))["trigger"]["text"]
print(json.dumps({"slug": re.sub(r"[^a-z0-9]+", "-", text.lower()).strip("-")}))

C#

Goal

Write a C# block as a .NET file-based app. You don’t need a project file. Time: 10 minutes. Needs: .NET SDK 10 or later.

language: csharp
using System;
using System.IO;
using System.Text.Json;
using System.Text.Json.Nodes;

var path = Environment.GetEnvironmentVariable("TERMORQ_INPUT")
    ?? throw new InvalidOperationException("TERMORQ_INPUT is missing");
using var input = JsonDocument.Parse(File.ReadAllText(path));
var value = input.RootElement.GetProperty("trigger").GetProperty("text");
if (value.ValueKind != JsonValueKind.String)
    throw new InvalidOperationException("trigger.text must be a string");

Console.WriteLine(new JsonObject { ["message"] = value.GetString() }.ToJsonString());

Things to know

  • The runner runs dotnet run snippet.cs using file-based apps. Use top-level statements: you don’t need a class or Main.
  • Build output with JsonObject.ToJsonString(). Reflection-based JsonSerializer.Serialize(anonymousObject) may be disabled in file-based apps.
  • The first run of a snippet compiles it, so expect a few seconds of start-up.
  • An unhandled exception exits with a non-zero code, so the block fails.

Reading plain text

For Execute step with plain text:

using System.Text.Json.Nodes;
var text = File.ReadAllText(Environment.GetEnvironmentVariable("TERMORQ_INPUT")!);
Console.WriteLine(new JsonObject { ["message"] = text }.ToJsonString());
Check your run

Execute step prints {"message":"Hello from curl"}.

Exercise

Publish reversed: the text with its characters in reverse order.

Details
using System.Text.Json;
using System.Text.Json.Nodes;
using var doc = JsonDocument.Parse(File.ReadAllText(Environment.GetEnvironmentVariable("TERMORQ_INPUT")!));
var t = doc.RootElement.GetProperty("trigger").GetProperty("text").GetString() ?? "";
var arr = t.ToCharArray(); Array.Reverse(arr);
Console.WriteLine(new JsonObject { ["reversed"] = new string(arr) }.ToJsonString());

Rust

Goal

Write a Rust block that works without any crates. Time: 10 minutes. Needs: rustc with edition 2024 support.

language: rust
outputs:
  type: object
  properties:
    input_bytes:
      type: number
use std::{env, error::Error, fs};

fn main() -> Result<(), Box<dyn Error>> {
    let path = env::var("TERMORQ_INPUT")?;
    let input = fs::read(path)?;
    println!("{{\"input_bytes\":{}}}", input.len());
    Ok(())
}

Things to know

  • The runner compiles with rustc --edition 2024 -O, then runs the binary. There’s no Cargo, so crates such as serde_json aren’t available.
  • The standard library has no JSON parser. Use Rust for byte, line, or number processing, and use another language for JSON field transformations.
  • Use format! and println! for output. Escape { and } by doubling them. Only interpolate values that can’t contain quotes, backslashes, or newlines, or escape them yourself.
  • Returning Err from main exits with a non-zero code, so the block fails.
Check your run

With the exact compact sample {"trigger":{"text":"Hello from curl"}} (no trailing newline), the output is {"input_bytes":38}. That’s the UTF-8 byte count of the whole file.

Exercise

Publish lines (number): the number of lines in the input file. Test it with multi-line plain text in Step input.

Details
use std::{env, fs};
fn main() {
    let text = fs::read_to_string(env::var("TERMORQ_INPUT").unwrap()).unwrap();
    println!("{{\"lines\":{}}}", text.lines().count());
}

JavaScript

Goal

Write a JavaScript block as an ES module. Time: 5 minutes. Needs: Node.js.

language: javascript
import { readFileSync } from "node:fs";

const path = process.env.TERMORQ_INPUT;
if (!path) throw new Error("TERMORQ_INPUT is missing");
const data = JSON.parse(readFileSync(path, "utf8"));
const text = data?.trigger?.text;
if (typeof text !== "string") {
  throw new Error("trigger.text must be a string");
}
console.log(JSON.stringify({ message: text }));

Things to know

  • The runner writes your code to snippet.mjs, so it’s an ES module: use import, not require. Top-level await works.
  • For Code file, .js and .mjs are inferred as JavaScript.
  • Node built-ins (node:fs, node:path, and fetch) are available. npm packages aren’t installed for you.
  • Never use JSON.stringify(obj, null, 2) for the final line, because it prints several lines.
Check your run

Execute step prints {"message":"Hello from curl"}.

Exercise

Publish chars: an array of the unique characters in the text, sorted.

Details
import { readFileSync } from "node:fs";
const { trigger } = JSON.parse(readFileSync(process.env.TERMORQ_INPUT, "utf8"));
console.log(JSON.stringify({ chars: [...new Set(trigger.text)].sort() }));

Declare chars: { type: array, items: { type: string } }.

TypeScript

Goal

Write a TypeScript block that runs on Node’s native type stripping. Time: 10 minutes. Needs: Node.js 22.6+ (or tsx).

language: typescript
import { readFileSync } from "node:fs";

type Input = { trigger?: { text?: unknown } };
const path = process.env.TERMORQ_INPUT;
if (!path) throw new Error("TERMORQ_INPUT is missing");
const data = JSON.parse(readFileSync(path, "utf8")) as Input | null;
const text = data?.trigger?.text;
if (typeof text !== "string") {
  throw new Error("trigger.text must be a string");
}
console.log(JSON.stringify({ message: text }));

Things to know

  • Termorq prefers native Node (22.6+), which strips types without type-checking them and ignores tsconfig.json. If native support isn’t available, it uses a tsx executable instead.
  • Use only erasable syntax: type annotations, type, interface, and as. Avoid enum, namespace, and parameter properties, because native stripping can’t run them. See Node.js TypeScript support.
  • Types don’t validate JSON at run time. Keep the typeof checks.
Check your run

Execute step prints {"message":"Hello from curl"}. {"trigger":{}} fails with trigger.text must be a string.

Exercise

Accept an inputs.names array (declare inputs: names: {type: array, items: {type: string}} and uses: [inputs.names]), then publish count and first.

Details
import { readFileSync } from "node:fs";
type In = { inputs: { names: string[] } };
const { inputs } = JSON.parse(readFileSync(process.env.TERMORQ_INPUT!, "utf8")) as In;
if (!Array.isArray(inputs.names)) throw new Error("inputs.names must be an array");
console.log(JSON.stringify({ count: inputs.names.length, first: inputs.names[0] ?? "" }));

Test it with the Step input {"inputs":{"names":["Ada","Lin"]}}.