Composition
Composition allows an action to use other services as part of its implementation. The caller purchases the resulting service at your advertised price, while your action allocates parts of that budget to the providers it uses. You remain responsible for the combined result and receive the rating for the call the buyer made.
The budget rule
A child call receives its allocation from the remaining budget of its parent. If the requested action costs more than the parent has available, the kernel rejects the child call before it executes:
error: wasm execution failed: run failed: parent trace has 100000 credits, action costs 750000
The current WebAssembly host stops the module on this refusal, causing the
parent call to fail and refund its unused budget. Check expected child prices
when designing the action. An HTTP implementation receives an error from
POST /v1/call and can decide how to handle it.
Applying the same budget rule at each level bounds execution spending throughout the call tree. Separate value transfers and remote ticket stakes use the immediate caller’s balance, as described in Earnings.
Composing from WebAssembly
A wasm action runs a compiled WebAssembly module inside the kernel’s sandbox.
The module has no direct access to the filesystem, network, environment, or
credentials. It interacts with Juice through four host functions:
| Host function | Effect |
|---|---|
JuiceCall(action, args) |
call another action from this call’s budget |
JuiceStepCreate(partialArgs, requiredCaller, action) |
set work aside for a named party |
JuiceStepComplete(stepID, input) |
complete a step this call created |
JuiceLog(level, msg) |
write a log record against this call |
A module can reach a web service by calling sys/web or a registered HTTP
action. These requests use the ordinary action budget and the kernel’s outbound
network policy.
Writing and compiling a handler
The supplied compilation interface expects a Handle function. It supplies
the surrounding module code, including the entry point and host bindings.
This example calls sys/time and combines its result with a note from the input:
func Handle(in map[string]any) (map[string]any, error) {
raw, err := JuiceCall("sys/time", []byte("{}"))
if err != nil {
return nil, err
}
var now struct {
ISO string `json:"iso"`
}
if err := json.Unmarshal(raw, &now); err != nil {
return nil, err
}
return map[string]any{"note": in["note"], "stamped_at": now.ISO}, nil
}
The supplied imports include encoding/json, strings, strconv, math,
sort, and errors; they do not include fmt. Save the function in
handler.go, then call sys/tinygo/compile. Its result contains a base64
module, which the following pipeline decodes into a file:
$ juice run sys/tinygo/compile "$(jq -Rs '{source: .}' handler.go)" --json \
| jq -r .result.artifact | base64 -d > stamp.wasm
Register the decoded module as a WebAssembly action, then enable it and choose its audience:
$ juice action create stamp --kind wasm --artifact stamp.wasm --price 1 \
--description "Stamp a note with the current time" \
--input-schema '{"type":"object","properties":{"note":{"type":"string","description":"text to stamp"}},"required":["note"]}'
$ juice action enable bob/stamp
$ juice action update bob/stamp --visibility local
The action returns the input note together with the timestamp obtained by its child call:
$ juice run bob/stamp '{"note":"invoice 42"}'
result: {
"note": "invoice 42",
"stamped_at": "2026-09-14T12:06:04Z"
}
…
A source compilation failure is reported in the compiler action’s result. The compilation service is still charged, since it performed the requested compilation attempt:
result: {
"status": "failure",
"diagnostics": [ … ]
}
If the kernel lacks the toolchain, the compilation action is unavailable and
reports an invalid state without charging. A module compiled elsewhere can also
be registered from its .wasm file.
Composing from an HTTP endpoint
An HTTP implementation can request child calls through a callback to the kernel. The dispatched request supplies a callback address and a capability authorizing work within that call, so the endpoint does not need a saved Juice login. When a callback address is available, the request includes:
X-Juice-Callback: http://127.0.0.1:4040
X-Juice-Capability: <trace-id>.<signature>
To make a child call, send the capability in the same header to the callback address. The kernel allocates the child’s price from the current call’s budget:
POST http://127.0.0.1:4040/v1/call
X-Juice-Capability: <the capability, verbatim>
Content-Type: application/json
{"action": "carol/extract", "args": {"text": "…"}}
{"result": {"…"}, "tx_id": "…", "trace_id": "…"}
The current HTTP interface expects the X-Juice-Capability header. Supplying
the capability as an Authorization: Bearer token is rejected:
{"code":"unauthenticated","error":"capability required"}
The header also authorizes step creation and completion through
POST /v1/steps and POST /v1/steps/{id}/complete, subject to the step rules.
Completion is restricted to steps created by this trace and addressed to the
executing action’s owner.
The capability acts with the action owner’s authority inside the current call. It cannot spend another call’s budget, access account management, or submit ratings, and it expires when the call settles. An endpoint can therefore serve several kernels by using the callback information in each request.
This capability remains local to the executing kernel. Remote buyers invoke your action through ordinary federation; the endpoint’s child calls still use the budget and callback supplied by your own kernel.
Reaching your own private actions
Child calls are authorized as the composing action’s owner. Your public action can therefore use your private helpers even when another user funds the process. It does not gain access to that user’s private actions: paying for a service does not grant its implementation the payer’s permissions.