Tasklet ABI
tasklet-profile-v0 is the contract between a WASM tasklet and the grafOS runtime. If you write tasklets in Rust against the grafos-std SDK, the SDK handles all of this for you. This page is for the WASM author who wants to ship tasklets in another language (AssemblyScript, TinyGo, Zig, hand-written WAT) — what the runtime expects from your module.
Profile version: v0
The wire profile is tasklet-profile-v0. The version pins:
- which imports the runtime exposes
- which exports the module must provide
- the lifecycle hook ordering
- the output-artifact contract
- fuel and memory bounds
A future v1 may add new imports or exports. v0 modules will keep running unchanged.
Memory model
Each tasklet is its own WASM linear memory. The runtime allocates the memory at module instantiation; the size is bounded by the lease quota declared in grafos.toml (default: 32 MiB, max: configurable per-program). The module must not request more linear memory than the runtime allocated.
The runtime never reads or writes the module’s linear memory directly. Data flows in/out via the import functions documented below.
There is no WASI filesystem. There is no WASI clock (the runtime exposes a deterministic clock import; see below). There is no thread, no mutex, no atomics across host boundaries. WebAssembly multi-value is fine; SIMD is fine where the cell’s runtime supports it.
Fuel
Every WASM instruction the tasklet executes consumes one unit of fuel. The fuel budget is set per-tasklet in grafos.toml (default: 500_000 instructions). When fuel runs out, the module traps with out_of_fuel; the runtime returns a typed fuel_exhausted error to the program.
Fuel is the deterministic load shedder. A runaway loop or pathological input does not starve the cell — it fails the tasklet cleanly.
Required exports
The module must export exactly these symbols:
(func (export "_grafos_tasklet_init") (param i32 i32)) ;; init context(func (export "_grafos_tasklet_run") (param i32 i32) (result i32)) ;; main entry(func (export "_grafos_tasklet_finalize")) ;; cleanup(memory (export "memory") 1) ;; linear memoryThe lifecycle is init → run → finalize. init and finalize may be no-ops if the module has nothing to set up or tear down. run is where the work happens; the (param i32 i32) is (input_ptr, input_len) and the (result i32) is the byte length of the output payload (which the runtime then reads from a known address).
Input contract
The runtime writes the tasklet input to the module’s linear memory at offset 0x10000 (64 KiB), then calls _grafos_tasklet_run(0x10000, len). The module reads that range and produces output.
The 64 KiB offset is fixed in v0 — modules can rely on it as a stable input address.
Output contract
The tasklet writes its output payload at offset 0x10000 + input_len (immediately after the input) and returns the byte length from _grafos_tasklet_run. The runtime then:
- Reads
lenbytes starting atoutput_ptrfrom the module’s linear memory. - Persists them as
output.b64(base64-encoded) plusresponse.json(a small wrapper with run metadata + structured output) under the run’s artifact directory.
If the tasklet returns 0, the runtime persists empty artifacts and finalizes successfully (the contract says 0 is “no payload”, not “error” — for errors, use the trap path).
Imports
The runtime exposes these imports under the grafos module:
;; deterministic monotonic clock (nanoseconds since tasklet start)(import "grafos" "now_ns" (func (param) (result i64)))
;; capability-token-bound memory read(import "grafos" "mem_read" (func (param i32 i32 i64 i64) (result i32)));; buf_ptr buf_len mem_id offset
;; capability-token-bound memory write(import "grafos" "mem_write" (func (param i32 i32 i64 i64) (result i32)));; buf_ptr buf_len mem_id offset
;; structured log line (one event per call)(import "grafos" "log" (func (param i32 i32 i32)));; ptr len levelToken presentation is automatic — the runtime tracks which leases the tasklet was admitted under and presents the appropriate token on each mem_read / mem_write. The tasklet does not see the token bytes.
Failure return codes:
0= success- non-zero = typed error (full code list in
/spec/fabricbios-wire-encoding-v0)
What you cannot do
- No host filesystem. No
fopen,fread. The fabric’s storage abstractions go throughmem_read/mem_writeagainst leased Block resources viagrafos-store-like wrappers (you’d replicate that contract in your language). - No host network. Same idea — leased Net resources, capability-token-bound ops. v0 has no direct network imports for tasklets.
- No threads. WASM has no native thread story; the runtime does not paper over that. Concurrent work is structured by the program-level graph (multiple tasklets running on multiple cells), not by intra-tasklet threads.
- No
evalor self-modifying code. A tasklet’s WASM module is hashed at deploy time; the SHA-256 is in the manifest. The runtime refuses to admit a module whose hash has changed.
Authoring in Rust (the easy path)
If you don’t have a hard reason to use another language, write Rust:
use grafos_std::{tasklet, Output};
#[tasklet]fn metadata(input: &[u8]) -> Output { Output::json(serde_json::json!({ "size_bytes": input.len() }))}The #[tasklet] macro generates the v0-compliant exports and handles input/output framing. You write a function; the SDK does the ABI work.
Authoring in TinyGo / AssemblyScript / Zig
You’ll be writing the exports by hand. Match the export names exactly, declare them with the right signatures, and target wasm32-unknown-unknown (or the equivalent for your toolchain). The runtime trap codes are language-agnostic; if your trap doesn’t match the v0 contract, the runtime treats it as tasklet_panicked and persists an error artifact.
There’s no out-of-the-box example for non-Rust tasklets today. If you ship one, please file an issue against tenura-systems/tenura so we can add a cookbook recipe.
Where to next
- Cookbook → hello-tasklet — the smallest working Rust tasklet.
/spec/fabricbios-wire-encoding-v0— the byte-level error code list and any v0 vs v1 differences once a v1 ships.grafos-std— the Rust crate that wraps this ABI.