A comprehensive Zig SDK for Cloudflare Workers. Write Workers, Durable Objects, Workflows, and more — entirely in Zig.
Built on JSPI (JavaScript Promise Integration), workers-zig lets you call async Workers APIs with normal, synchronous Zig code. No callbacks, no event loops, no allocator gymnastics — just straightforward Zig.
step.do(), step.sleep(), step.waitForEvent()std.fs read/write to /tmp, std.process.Environ reads worker vars, std.time, std.crypto.randomStreamingResponsezig build produces worker.wasm + entry.js + shim.js, ready for wrangler deploymkdir my-worker && cd my-worker
zig init
zig fetch --save git+https://github.com/nilslice/workers-zig
Or manually add it to your build.zig.zon:
.{
.name = .my_worker,
.version = "0.1.0",
.fingerprint = 0xYOUR_FINGERPRINT,
.minimum_zig_version = "0.16.0",
.dependencies = .{
.@"workers-zig" = .{
.url = "https://github.com/nilslice/workers-zig/archive/refs/heads/main.tar.gz",
.hash = "...", // zig build will tell you the correct hash
},
},
.paths = .{ "build.zig", "build.zig.zon", "src" },
}
const std = @import("std");
const workers_zig = @import("workers-zig");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const dep = b.dependency("workers-zig", .{});
const exe = workers_zig.addWorker(b, dep, b.path("src/main.zig"), .{
.name = "worker",
.optimize = optimize,
});
b.installArtifact(exe);
}
// src/main.zig
const workers = @import("workers-zig");
const Request = workers.Request;
const Response = workers.Response;
const Env = workers.Env;
const Context = workers.Context;
pub fn fetch(request: *Request, env: *Env, _: *Context) !Response {
_ = request;
_ = env;
return Response.ok("Hello from Zig!");
}
Create a wrangler.toml:
name = "my-worker"
main = "zig-out/bin/entry.js"
[build]
command = "zig build -Doptimize=ReleaseSmall"
zig build && npx wrangler dev
workers-zig uses a 3-layer architecture:
Your Zig code
↓ imports "workers-zig"
src/entry.zig — wasm export dispatcher (fetch, scheduled, queue, etc.)
↓ calls extern "env" functions
src/js.zig — FFI declarations (extern fn → JS imports)
↓ imported by
js/shim.js — JS glue: maps FFI calls to Workers runtime APIs
↓ wrapped with
JSPI (Suspending/Promising) — async JS calls appear synchronous to Zig
JSPI is the key innovation: when your Zig code calls an async API (e.g., kv.getText()), the WebAssembly stack suspends, the JS promise resolves, and execution resumes in Zig — all transparently. Your Zig code reads like synchronous, blocking I/O.
Build-time code generation: zig build compiles your code to wasm, then gen_entry parses the wasm export section to discover Durable Object classes (via do_<Name>_fetch exports) and Workflow classes (via wf_<Name>_run exports), generating the correct entry.js with all necessary factory functions.
Export these public functions from your src/main.zig to handle different event types:
// HTTP requests (required)
pub fn fetch(request: *workers.Request, env: *workers.Env, ctx: *workers.Context) !workers.Response
// Cron triggers
pub fn scheduled(event: *workers.ScheduledEvent, env: *workers.Env, ctx: *workers.Context) !void
// Queue consumer
pub fn queue(batch: *workers.Queue.Batch, env: *workers.Env, ctx: *workers.Context) !void
// Tail worker (log consumer)
pub fn tail(events: []const workers.Tail.TraceItem, env: *workers.Env, ctx: *workers.Context) !void
// Email routing
pub fn email(message: *workers.EmailMessage, env: *workers.Env, ctx: *workers.Context) !void
All handlers are auto-detected at compile time — just export the function and the framework handles the rest.
The built-in router provides path-parameter extraction and method-based routing with zero allocations:
const workers = @import("workers-zig");
const Request = workers.Request;
const Response = workers.Response;
const Env = workers.Env;
const Context = workers.Context;
const Router = workers.Router;
pub fn fetch(request: *Request, env: *Env, _: *Context) !Response {
return Router.serve(request, env, &.{
Router.get("/", handleIndex),
Router.get("/users/:id", getUser),
Router.post("/users", createUser),
Router.all("/health", healthCheck),
}) orelse Response.err(.not_found, "Not Found");
}
fn getUser(_: *Request, _: *Env, params: *Router.Params) !Response {
const id = params.get("id") orelse "unknown";
return Response.ok(id);
}
Supported patterns:
/api/users/users/:id/posts/:post_id (up to 8 params)/static/*get, post, put, delete, patch, head, allconst kv = try env.kv("MY_KV");
// Read
const value = try kv.getText("key");
// Write
kv.put("key", "value");
// List
const result = try kv.list(.{ .prefix = "user:" });
// Delete
kv.delete("key");
const bucket = try env.r2("MY_BUCKET");
// Upload
_ = try bucket.put("file.txt", body, .{ .content_type = "text/plain" });
// Download
if (try bucket.get("file.txt")) |obj| {
const data = obj.body;
}
// List
const objects = try bucket.listObjects(.{ .prefix = "uploads/" });
const db = try env.d1("MY_DB");
const results = try db.query("SELECT * FROM users WHERE id = ?", .{42});
Define a Durable Object as a Zig struct with fetch (and optionally alarm):
pub const Counter = struct {
state: workers.DurableObject.State,
env: workers.Env,
pub fn fetch(self: *Counter, request: *workers.Request) !workers.Response {
var storage = self.state.storage();
const count = try storage.get("count");
// ...
return workers.Response.ok(count orelse "0");
}
pub fn alarm(self: *Counter) !void {
// periodic alarm logic
}
};
Use from your fetch handler:
const ns = try env.durableObject("COUNTER");
const id = ns.idFromName("my-instance");
const stub = ns.get(id);
var resp = try stub.fetch("http://do/increment", .{});
The build system auto-detects DO classes and generates the necessary JS glue. Configure in wrangler.toml:
[durable_objects]
bindings = [{ name = "COUNTER", class_name = "Counter" }]
[[migrations]]
tag = "v1"
new_classes = ["Counter"]
const ai = try env.ai("AI");
// Text generation
const result = try ai.textGeneration("@cf/meta/llama-3.1-8b-instruct", .{
.prompt = "Explain WebAssembly in one sentence",
.max_tokens = 100,
});
const text = result.response orelse "no response";
// Streaming
var reader = try ai.textGenerationStream("@cf/meta/llama-3.1-8b-instruct", .{
.prompt = "Write a haiku",
.max_tokens = 60,
});
while (try reader.next()) |chunk| {
stream.write(chunk);
}
// Embeddings
const embeddings = try ai.textEmbeddings("@cf/baai/bge-base-en-v1.5", .{
.text = &.{"hello world"},
});
Define a workflow class:
pub const MyWorkflow = struct {
pub fn run(event: workers.Workflow.Event, step: workers.Workflow.Step) ![]const u8 {
const result = try step.do("process", .{}, struct {
fn callback() []const u8 {
return "computed value";
}
}.callback);
try step.sleep("pause", std.time.ms_per_s * 5);
return result;
}
};
Create and manage instances from your fetch handler:
const wf = try env.workflow("MY_WORKFLOW");
const instance = try wf.create(.{ .input = "data" }, .{});
const status = try instance.status();
Configure in wrangler.toml:
[[workflows]]
name = "my-workflow"
binding = "MY_WORKFLOW"
class_name = "MyWorkflow"
const arts = try env.artifacts("ARTIFACTS");
// Create a repo
const result = try arts.create("my-repo", .{});
const remote = result.remote;
const token = result.token;
// Get a repo handle
if (try arts.get("my-repo")) |repo| {
// Get repo info (JSON)
const info_json = try repo.info();
// Mint a read token valid for 1 hour
const tok_json = try repo.createToken(.read, 3600);
// Fork into a new repo
const fork_json = try repo.fork("my-repo-fork", .{
.description = "Fork for testing",
.default_branch_only = true,
});
}
// List repos
const list_json = try arts.list(.{ .limit = 20 });
// Import a public GitHub repo
const result = try arts.import(.{
.source = .{
.url = "https://github.com/nilslice/workers-zig",
.branch = "main",
.depth = 1,
},
.target = .{
.name = "my-mirror",
},
});
// Access imported repo via result.repo
const info = try result.repo.info();
// Delete a repo
_ = arts.delete("my-repo");
Configure the binding in wrangler.toml:
[[artifacts]]
binding = "ARTIFACTS"
namespace = "default"
var ws = workers.WebSocket.init(allocator);
ws.accept();
ws.sendText("connected");
while (ws.receive()) |event| {
switch (event.type()) {
.text => {
const msg = try event.text();
ws.sendText(msg); // echo
},
.close => {
ws.close(1000, "bye");
break;
},
else => {},
}
}
return ws.response();
// Plaintext or simple TLS via cloudflare:sockets
var socket = try workers.Socket.connect(allocator, "example.com", 80, .{});
socket.write("GET / HTTP/1.0\r\n\r\n");
const data = try socket.read();
socket.close();
// Extended TLS via node:tls — custom CA, SNI override, or mTLS
var tls = try workers.Socket.connectTls(allocator, "api.example.com", 443, .{
.servername = "api.example.com",
.cert = try env.get("CLIENT_CERT"),
.key = try env.get("CLIENT_KEY"),
});
defer tls.close();
connectTls requires compatibility_flags = ["nodejs_compat"] in your wrangler.toml.
// Producer
const queue = try env.queue("MY_QUEUE");
queue.send("message body");
// Consumer (export the handler)
pub fn queue(batch: *workers.Queue.Batch, env: *workers.Env, ctx: *workers.Context) !void {
for (batch.messages()) |msg| {
const body = msg.body();
msg.ack();
}
}
var stream = workers.StreamingResponse.start(.{});
stream.setHeader("content-type", "text/event-stream");
stream.write("data: hello\n\n");
stream.write("data: world\n\n");
stream.close();
return stream.response();
Incoming email routing:
pub fn email(message: *workers.EmailMessage, _: *workers.Env, _: *workers.Context) !void {
const from = message.from();
const to = message.to();
const size = message.rawSize();
// Forward, reply, or reject
try message.forward("admin@example.com");
}
Outbound email:
const mailer = try env.sendEmail("EMAIL");
try mailer.send(.{
.from = "noreply@example.com",
.to = "user@example.com",
.subject = "Hello from Zig",
.body_text = "Plain text body",
.body_html = "<h1>Hello!</h1>",
});
pub fn tail(events: []const workers.Tail.TraceItem, env: *workers.Env, _: *workers.Context) !void {
for (events) |item| {
for (item.logs) |entry| {
workers.log("trace: {s}", .{entry.message});
}
}
}
| Binding | Access | Description |
|---|---|---|
| Cache | workers.Cache | Cache API (put, match, delete) |
| Vectorize | env.vectorize("INDEX") | Vector database for embeddings |
| Hyperdrive | env.hyperdrive("DB") | Connection pooling for databases |
| Analytics Engine | env.analyticsEngine("AE") | Write analytics data points |
| Rate Limiting | env.rateLimit("RL") | Rate limiter binding |
| Service Bindings | env.serviceBinding("SVC") | Call other Workers |
| Dispatch Namespace | env.dispatchNamespace("NS") | Workers for Platforms |
| Crypto | workers.Crypto | Web Crypto (digest, random, sign, verify) |
| HTMLRewriter | workers.HTMLRewriter | Streaming HTML transformation |
| FormData | workers.FormData | Multipart form data parsing |
| EventSource | workers.EventSource | Server-Sent Events (SSE) |
| Artifacts | env.artifacts("ARTIFACTS") | Durable Git repos (create, fork, tokens) |
| Container | workers.Container | Container Workers |
// Allocators: `env.allocator` is a per-request arena freed when the
// request ends — prefer it inside `fetch` handlers. `workers.allocator`
// is the process-lifetime wasm heap, for DOs / long-lived state.
const alloc = env.allocator;
// Outbound HTTP fetch
var resp = try workers.fetch(alloc, "https://api.example.com/data", .{
.method = .POST,
.body = "{\"key\": \"value\"}",
});
defer resp.deinit();
const body = try resp.text();
// Current time (milliseconds since epoch)
const timestamp = workers.now();
// Sleep (JSPI-suspending)
workers.sleep(1000);
// Console logging
workers.log("request from {s}", .{request.cf().country orelse "unknown"});
See the examples/ directory:
| Example | Description |
|---|---|
| 01-hello | Router-based hello world with path params |
| 02-kv-r2 | KV and R2 storage operations |
| 03-durable-object | Durable Object counter with increment/get/reset |
| 04-websocket-ai | WebSocket echo server + Workers AI text generation |
| 05-tcp-echo | Outbound TCP socket + HTTP fetch |
Each example is a standalone project. To run one:
cd examples/01-hello
zig build && npx wrangler dev
workers-zig/
├── build.zig # Build system with addWorker() helper
├── build.zig.zon # Package manifest
├── src/
│ ├── root.zig # Public API surface
│ ├── entry.zig # Wasm export dispatcher
│ ├── js.zig # FFI extern declarations
│ ├── gen_entry.zig # Build tool: wasm → entry.js
│ ├── Router.zig # HTTP router
│ ├── Workflow.zig # Workflows API
│ ├── ... # One file per binding
│ └── Tail.zig # Tail workers
├── js/
│ └── shim.js # JS glue layer (JSPI, handle table, API mapping)
└── examples/ # Standalone example workers
MIT
14 commits
Zig
76.3%
JavaScript
18.2%
Shell
5.5%
A comprehensive Zig SDK for Cloudflare Workers. Write Workers, Durable Objects, Workflows, and more — entirely in Zig.
Built on JSPI (JavaScript Promise Integration), workers-zig lets you call async Workers APIs with normal, synchronous Zig code. No callbacks, no event loops, no allocator gymnastics — just straightforward Zig.
step.do(), step.sleep(), step.waitForEvent()std.fs read/write to /tmp, std.process.Environ reads worker vars, std.time, std.crypto.randomStreamingResponsezig build produces worker.wasm + entry.js + shim.js, ready for wrangler deploymkdir my-worker && cd my-worker
zig init
zig fetch --save git+https://github.com/nilslice/workers-zig
Or manually add it to your build.zig.zon:
.{
.name = .my_worker,
.version = "0.1.0",
.fingerprint = 0xYOUR_FINGERPRINT,
.minimum_zig_version = "0.16.0",
.dependencies = .{
.@"workers-zig" = .{
.url = "https://github.com/nilslice/workers-zig/archive/refs/heads/main.tar.gz",
.hash = "...", // zig build will tell you the correct hash
},
},
.paths = .{ "build.zig", "build.zig.zon", "src" },
}
const std = @import("std");
const workers_zig = @import("workers-zig");
pub fn build(b: *std.Build) void {
const optimize = b.standardOptimizeOption(.{});
const dep = b.dependency("workers-zig", .{});
const exe = workers_zig.addWorker(b, dep, b.path("src/main.zig"), .{
.name = "worker",
.optimize = optimize,
});
b.installArtifact(exe);
}
// src/main.zig
const workers = @import("workers-zig");
const Request = workers.Request;
const Response = workers.Response;
const Env = workers.Env;
const Context = workers.Context;
pub fn fetch(request: *Request, env: *Env, _: *Context) !Response {
_ = request;
_ = env;
return Response.ok("Hello from Zig!");
}
Create a wrangler.toml:
name = "my-worker"
main = "zig-out/bin/entry.js"
[build]
command = "zig build -Doptimize=ReleaseSmall"
zig build && npx wrangler dev
workers-zig uses a 3-layer architecture:
Your Zig code
↓ imports "workers-zig"
src/entry.zig — wasm export dispatcher (fetch, scheduled, queue, etc.)
↓ calls extern "env" functions
src/js.zig — FFI declarations (extern fn → JS imports)
↓ imported by
js/shim.js — JS glue: maps FFI calls to Workers runtime APIs
↓ wrapped with
JSPI (Suspending/Promising) — async JS calls appear synchronous to Zig
JSPI is the key innovation: when your Zig code calls an async API (e.g., kv.getText()), the WebAssembly stack suspends, the JS promise resolves, and execution resumes in Zig — all transparently. Your Zig code reads like synchronous, blocking I/O.
Build-time code generation: zig build compiles your code to wasm, then gen_entry parses the wasm export section to discover Durable Object classes (via do_<Name>_fetch exports) and Workflow classes (via wf_<Name>_run exports), generating the correct entry.js with all necessary factory functions.
Export these public functions from your src/main.zig to handle different event types:
// HTTP requests (required)
pub fn fetch(request: *workers.Request, env: *workers.Env, ctx: *workers.Context) !workers.Response
// Cron triggers
pub fn scheduled(event: *workers.ScheduledEvent, env: *workers.Env, ctx: *workers.Context) !void
// Queue consumer
pub fn queue(batch: *workers.Queue.Batch, env: *workers.Env, ctx: *workers.Context) !void
// Tail worker (log consumer)
pub fn tail(events: []const workers.Tail.TraceItem, env: *workers.Env, ctx: *workers.Context) !void
// Email routing
pub fn email(message: *workers.EmailMessage, env: *workers.Env, ctx: *workers.Context) !void
All handlers are auto-detected at compile time — just export the function and the framework handles the rest.
The built-in router provides path-parameter extraction and method-based routing with zero allocations:
const workers = @import("workers-zig");
const Request = workers.Request;
const Response = workers.Response;
const Env = workers.Env;
const Context = workers.Context;
const Router = workers.Router;
pub fn fetch(request: *Request, env: *Env, _: *Context) !Response {
return Router.serve(request, env, &.{
Router.get("/", handleIndex),
Router.get("/users/:id", getUser),
Router.post("/users", createUser),
Router.all("/health", healthCheck),
}) orelse Response.err(.not_found, "Not Found");
}
fn getUser(_: *Request, _: *Env, params: *Router.Params) !Response {
const id = params.get("id") orelse "unknown";
return Response.ok(id);
}
Supported patterns:
/api/users/users/:id/posts/:post_id (up to 8 params)/static/*get, post, put, delete, patch, head, allconst kv = try env.kv("MY_KV");
// Read
const value = try kv.getText("key");
// Write
kv.put("key", "value");
// List
const result = try kv.list(.{ .prefix = "user:" });
// Delete
kv.delete("key");
const bucket = try env.r2("MY_BUCKET");
// Upload
_ = try bucket.put("file.txt", body, .{ .content_type = "text/plain" });
// Download
if (try bucket.get("file.txt")) |obj| {
const data = obj.body;
}
// List
const objects = try bucket.listObjects(.{ .prefix = "uploads/" });
const db = try env.d1("MY_DB");
const results = try db.query("SELECT * FROM users WHERE id = ?", .{42});
Define a Durable Object as a Zig struct with fetch (and optionally alarm):
pub const Counter = struct {
state: workers.DurableObject.State,
env: workers.Env,
pub fn fetch(self: *Counter, request: *workers.Request) !workers.Response {
var storage = self.state.storage();
const count = try storage.get("count");
// ...
return workers.Response.ok(count orelse "0");
}
pub fn alarm(self: *Counter) !void {
// periodic alarm logic
}
};
Use from your fetch handler:
const ns = try env.durableObject("COUNTER");
const id = ns.idFromName("my-instance");
const stub = ns.get(id);
var resp = try stub.fetch("http://do/increment", .{});
The build system auto-detects DO classes and generates the necessary JS glue. Configure in wrangler.toml:
[durable_objects]
bindings = [{ name = "COUNTER", class_name = "Counter" }]
[[migrations]]
tag = "v1"
new_classes = ["Counter"]
const ai = try env.ai("AI");
// Text generation
const result = try ai.textGeneration("@cf/meta/llama-3.1-8b-instruct", .{
.prompt = "Explain WebAssembly in one sentence",
.max_tokens = 100,
});
const text = result.response orelse "no response";
// Streaming
var reader = try ai.textGenerationStream("@cf/meta/llama-3.1-8b-instruct", .{
.prompt = "Write a haiku",
.max_tokens = 60,
});
while (try reader.next()) |chunk| {
stream.write(chunk);
}
// Embeddings
const embeddings = try ai.textEmbeddings("@cf/baai/bge-base-en-v1.5", .{
.text = &.{"hello world"},
});
Define a workflow class:
pub const MyWorkflow = struct {
pub fn run(event: workers.Workflow.Event, step: workers.Workflow.Step) ![]const u8 {
const result = try step.do("process", .{}, struct {
fn callback() []const u8 {
return "computed value";
}
}.callback);
try step.sleep("pause", std.time.ms_per_s * 5);
return result;
}
};
Create and manage instances from your fetch handler:
const wf = try env.workflow("MY_WORKFLOW");
const instance = try wf.create(.{ .input = "data" }, .{});
const status = try instance.status();
Configure in wrangler.toml:
[[workflows]]
name = "my-workflow"
binding = "MY_WORKFLOW"
class_name = "MyWorkflow"
const arts = try env.artifacts("ARTIFACTS");
// Create a repo
const result = try arts.create("my-repo", .{});
const remote = result.remote;
const token = result.token;
// Get a repo handle
if (try arts.get("my-repo")) |repo| {
// Get repo info (JSON)
const info_json = try repo.info();
// Mint a read token valid for 1 hour
const tok_json = try repo.createToken(.read, 3600);
// Fork into a new repo
const fork_json = try repo.fork("my-repo-fork", .{
.description = "Fork for testing",
.default_branch_only = true,
});
}
// List repos
const list_json = try arts.list(.{ .limit = 20 });
// Import a public GitHub repo
const result = try arts.import(.{
.source = .{
.url = "https://github.com/nilslice/workers-zig",
.branch = "main",
.depth = 1,
},
.target = .{
.name = "my-mirror",
},
});
// Access imported repo via result.repo
const info = try result.repo.info();
// Delete a repo
_ = arts.delete("my-repo");
Configure the binding in wrangler.toml:
[[artifacts]]
binding = "ARTIFACTS"
namespace = "default"
var ws = workers.WebSocket.init(allocator);
ws.accept();
ws.sendText("connected");
while (ws.receive()) |event| {
switch (event.type()) {
.text => {
const msg = try event.text();
ws.sendText(msg); // echo
},
.close => {
ws.close(1000, "bye");
break;
},
else => {},
}
}
return ws.response();
// Plaintext or simple TLS via cloudflare:sockets
var socket = try workers.Socket.connect(allocator, "example.com", 80, .{});
socket.write("GET / HTTP/1.0\r\n\r\n");
const data = try socket.read();
socket.close();
// Extended TLS via node:tls — custom CA, SNI override, or mTLS
var tls = try workers.Socket.connectTls(allocator, "api.example.com", 443, .{
.servername = "api.example.com",
.cert = try env.get("CLIENT_CERT"),
.key = try env.get("CLIENT_KEY"),
});
defer tls.close();
connectTls requires compatibility_flags = ["nodejs_compat"] in your wrangler.toml.
// Producer
const queue = try env.queue("MY_QUEUE");
queue.send("message body");
// Consumer (export the handler)
pub fn queue(batch: *workers.Queue.Batch, env: *workers.Env, ctx: *workers.Context) !void {
for (batch.messages()) |msg| {
const body = msg.body();
msg.ack();
}
}
var stream = workers.StreamingResponse.start(.{});
stream.setHeader("content-type", "text/event-stream");
stream.write("data: hello\n\n");
stream.write("data: world\n\n");
stream.close();
return stream.response();
Incoming email routing:
pub fn email(message: *workers.EmailMessage, _: *workers.Env, _: *workers.Context) !void {
const from = message.from();
const to = message.to();
const size = message.rawSize();
// Forward, reply, or reject
try message.forward("admin@example.com");
}
Outbound email:
const mailer = try env.sendEmail("EMAIL");
try mailer.send(.{
.from = "noreply@example.com",
.to = "user@example.com",
.subject = "Hello from Zig",
.body_text = "Plain text body",
.body_html = "<h1>Hello!</h1>",
});
pub fn tail(events: []const workers.Tail.TraceItem, env: *workers.Env, _: *workers.Context) !void {
for (events) |item| {
for (item.logs) |entry| {
workers.log("trace: {s}", .{entry.message});
}
}
}
| Binding | Access | Description |
|---|---|---|
| Cache | workers.Cache | Cache API (put, match, delete) |
| Vectorize | env.vectorize("INDEX") | Vector database for embeddings |
| Hyperdrive | env.hyperdrive("DB") | Connection pooling for databases |
| Analytics Engine | env.analyticsEngine("AE") | Write analytics data points |
| Rate Limiting | env.rateLimit("RL") | Rate limiter binding |
| Service Bindings | env.serviceBinding("SVC") | Call other Workers |
| Dispatch Namespace | env.dispatchNamespace("NS") | Workers for Platforms |
| Crypto | workers.Crypto | Web Crypto (digest, random, sign, verify) |
| HTMLRewriter | workers.HTMLRewriter | Streaming HTML transformation |
| FormData | workers.FormData | Multipart form data parsing |
| EventSource | workers.EventSource | Server-Sent Events (SSE) |
| Artifacts | env.artifacts("ARTIFACTS") | Durable Git repos (create, fork, tokens) |
| Container | workers.Container | Container Workers |
// Allocators: `env.allocator` is a per-request arena freed when the
// request ends — prefer it inside `fetch` handlers. `workers.allocator`
// is the process-lifetime wasm heap, for DOs / long-lived state.
const alloc = env.allocator;
// Outbound HTTP fetch
var resp = try workers.fetch(alloc, "https://api.example.com/data", .{
.method = .POST,
.body = "{\"key\": \"value\"}",
});
defer resp.deinit();
const body = try resp.text();
// Current time (milliseconds since epoch)
const timestamp = workers.now();
// Sleep (JSPI-suspending)
workers.sleep(1000);
// Console logging
workers.log("request from {s}", .{request.cf().country orelse "unknown"});
See the examples/ directory:
| Example | Description |
|---|---|
| 01-hello | Router-based hello world with path params |
| 02-kv-r2 | KV and R2 storage operations |
| 03-durable-object | Durable Object counter with increment/get/reset |
| 04-websocket-ai | WebSocket echo server + Workers AI text generation |
| 05-tcp-echo | Outbound TCP socket + HTTP fetch |
Each example is a standalone project. To run one:
cd examples/01-hello
zig build && npx wrangler dev
workers-zig/
├── build.zig # Build system with addWorker() helper
├── build.zig.zon # Package manifest
├── src/
│ ├── root.zig # Public API surface
│ ├── entry.zig # Wasm export dispatcher
│ ├── js.zig # FFI extern declarations
│ ├── gen_entry.zig # Build tool: wasm → entry.js
│ ├── Router.zig # HTTP router
│ ├── Workflow.zig # Workflows API
│ ├── ... # One file per binding
│ └── Tail.zig # Tail workers
├── js/
│ └── shim.js # JS glue layer (JSPI, handle table, API mapping)
└── examples/ # Standalone example workers
MIT
14 commits
Zig
76.3%
JavaScript
18.2%
Shell
5.5%