Actions
In Goblin, actions are the universal unit of callable logic. What other languages split into functions, methods, lambdas, and procedures, Goblin calls one thing: an action.
You never have to remember whether something is a function or a method. If it does something and you can call it, it's an action.
Declaring Actions
Actions are declared with either act or action — they are identical. Use whichever reads more naturally.
act greet(name) :say("Hello, {name}!") xx action greet(name) :say("Hello, {name}!") xx
act greet(name) :say("Hello, {name}!") xx action greet(name) :say("Hello, {name}!") xx
Both produce exactly the same result. Goblin docs use act as the convention for brevity.
Inline Actions
Short actions can be written on a single line using =>. Inline actions do not use a closing xx or end.
act double(x) => return x * 2 act is_even(x) => return x % 2 == 0 act greet(name) => :say("Hello, {name}!")
act double(x) => return x * 2 act is_even(x) => return x % 2 == 0 act greet(name) => :say("Hello, {name}!")
Multiline Actions
Multiline actions close with either xx (crossbones) or end. Goblin convention prefers xx.
act describe(item, qty) label | "{qty}x {item}" :say(label) xx
act describe(item, qty) label | "{qty}x {item}" :say(label) xx
Both closers are valid, but xx is the Goblin style.
Parameters
Parameters are declared in parentheses. Defaults are set with |.
act greet(name, greeting | "Hello") :say("{greeting}, {name}!") xx greet("Sheriff") /// Hello, Sheriff! greet("Sheriff", "Howdy") /// Howdy, Sheriff!
act greet(name, greeting | "Hello") :say("{greeting}, {name}!") xx greet("Sheriff") /// Hello, Sheriff! greet("Sheriff", "Howdy") /// Howdy, Sheriff!
Return Values
Actions return the value of a return statement. Without one, they return nil.
act add(a, b) => return a + b result | add(3, 4) :say(result) /// 7
act add(a, b) => return a + b result | add(3, 4) :say(result) /// 7
Multiple return values are supported:
act min_max(items) return :grab_first(items), :grab_last(items) xx lo, hi | min_max([3, 1, 4, 1, 5, 9])
act min_max(items) return :grab_first(items), :grab_last(items) xx lo, hi | min_max([3, 1, 4, 1, 5, 9])
Calling Actions
Actions are called by name with arguments in parentheses.
act square(n) => return n * n square(5) /// 25
act square(n) => return n * n square(5) /// 25
Method-style calls use .:
result | items.sort() name | text.upper()
result | items.sort() name | text.upper()
Intrinsic Actions
Goblin ships with a large set of built-in actions called intrinsics. By convention, intrinsics are always called with a : prefix:
:say("hello") :len(items) :json_parse(raw) :run_cmd("goblin build.gbln")
:say("hello") :len(items) :json_parse(raw) :run_cmd("goblin build.gbln")
The : prefix is optional — say("hello") works too — but using it is strongly recommended. It tells you immediately that this is a built-in you can look up in the docs, rather than something defined in your current file. You never have to grep your codebase wondering where say came from.
:name(...), it's a Goblin built-in. If you see name(...), it's defined somewhere in your project.Invoking Actions Dynamically
Actions can be called by name at runtime using :invoke. This is useful when you want to pass behavior as data — for example, the predicate in collection operations like :grab_where.
:invoke accepts two forms:
act double(x) => return x * 2 /// pass args directly :invoke("double", 5) /// 10 /// or as an array :invoke("double", [5]) /// 10
act double(x) => return x * 2 /// pass args directly :invoke("double", 5) /// 10 /// or as an array :invoke("double", [5]) /// 10
Multiple arguments work the same way:
act add(a, b) => return a + b :invoke("add", 3, 4) /// 7 :invoke("add", [3, 4]) /// 7
act add(a, b) => return a + b :invoke("add", 3, 4) /// 7 :invoke("add", [3, 4]) /// 7
Namespaced actions use :::
:invoke("trailboss::rewrite_wiki_links", ctx)
:invoke("trailboss::rewrite_wiki_links", ctx)
This is how _where predicates work in collection operations — the action name is passed as a string and invoked on each element:
act is_big(x) => return x > 10 items | [3, 7, 12, 18, 2] big | :grab_where(items, "is_big") /// [12, 18]
act is_big(x) => return x > 10 items | [3, 7, 12, 18, 2] big | :grab_where(items, "is_big") /// [12, 18]
Summoning — Pipelines of Actions
:summon threads a value through a list of action names in sequence. Each action receives the output of the previous one. It is :invoke applied repeatedly down a list.
:summon(value, ["action_one", "action_two", "action_three"])
:summon(value, ["action_one", "action_two", "action_three"])
This is equivalent to:
value |= :invoke("action_one", value) value |= :invoke("action_two", value) value |= :invoke("action_three", value)
value |= :invoke("action_one", value) value |= :invoke("action_two", value) value |= :invoke("action_three", value)
In Sheriff, summon is how the entire build pipeline works. Each portal defines a schedule — an ordered list of action names — and summon threads the page context through all of them:
/// events.yall defines the schedule for this portal schedule | cfg["schedule"] /// thread ctx through every action in the schedule ctx |= :summon(ctx, schedule)
/// events.yall defines the schedule for this portal schedule | cfg["schedule"] /// thread ctx through every action in the schedule ctx |= :summon(ctx, schedule)
A schedule might look like:
schedule: - "frontier::strip_frontmatter" - "markdown_core::render" - "trailboss::rewrite_wiki_links" - "brindle::apply_layout" - "brindle::apply_tokens"
schedule: - "frontier::strip_frontmatter" - "markdown_core::render" - "trailboss::rewrite_wiki_links" - "brindle::apply_layout" - "brindle::apply_tokens"
Each action in the list receives ctx and returns a modified ctx. The final result is the fully built page.
Pre-build actions work the same way, but run once before any files are processed:
/// run a single pre-build action ret | :summon(pre_ctx, [act_name])
/// run a single pre-build action ret | :summon(pre_ctx, [act_name])
summon and invoke together give you a fully data-driven pipeline. The schedule lives in config — no code changes needed to add, remove, or reorder build steps.Namespace Actions
Actions inside modules are called with the :: namespace operator:
import game/hero as h h::create_hero() h::show_hero()
import game/hero as h h::create_hero() h::show_hero()
Summary
| Form | Syntax | Closes with |
|---|---|---|
| Inline | act name(params) => expr |
nothing |
| Multiline | act name(params) + body |
xx (preferred) or end |
| Keyword alias | action |
same as above |
| Intrinsic | :name(params) |
n/a — built-in |
| Dynamic | :invoke("name", args) |
n/a — built-in |
| Pipeline | :summon(value, ["a", "b"]) |
n/a — built-in |
Goblin's single act keyword replaces the entire zoo of function, def, fn, method, proc, lambda, and sub that other languages accumulate over time. One word. Every callable thing.