Next.js Authoring
Alpha - not yet shipped. The JavaScript / Next.js front-end is part
of the current alpha release and is not live in the public package. The
Python SDK is what's stable today; this page describes the authoring
model the JavaScript compiler will land with, and the
example app at example-app/javascript
is the running reference. Treat the syntax below as the locked-in
shape, not a stable API.
Waymark's first JavaScript target is Next.js. The runtime model is the same - workflows define durable control flow, actions define distributed work - but the front-end is a TypeScript compiler that lowers your authored source into the Waymark IR the Rust runtime already executes.
Status
The JavaScript SDK is in active development inside the alpha branch.
Most of the design is locked in and matches the Python authoring model -
the rest of this page walks through how to author against it. The
example app at example-app/javascript
is the running reference implementation, and it's what to crib from for
real wiring once the package ships.
Actions
Actions are regular exported async functions, marked by a // use action comment immediately above the declaration:
import { db } from "@/lib/db";
import { emailClient } from "@/lib/email";
// use action
export async function fetchUsers(userIds: string[]): Promise<User[]> {
return await db.user.findMany({
where: { id: { in: userIds } },
});
}
// use action
export async function sendWelcomeEmail(input: {
to: string;
}): Promise<EmailResult> {
return await emailClient.send({ to: input.to, subject: "Welcome" });
}
Authoring rules for the first pass:
- The
// use actioncomment sits directly above the exported function. - Actions are top-level named exports.
- Actions are
async. - Actions live in server-only modules.
The compiler scans for these comments before Next.js strips them, so the
declaration form matters. Arrow functions and class methods are deferred
to a later release; stick to named async function declarations for
now.
Workflows
Workflows are classes that extend Workflow. Inside run() you write
plain TypeScript control flow - if, for...of, while, try/catch,
local assignments, Promise.all(...). The compiler lowers that body
into Waymark IR at build time.
import { Workflow } from "@waymark/nextjs";
import { fetchUsers, sendWelcomeEmail } from "@/lib/actions/users";
export class WelcomeEmailWorkflow extends Workflow {
async run(userIds: string[]): Promise<EmailResult[]> {
const users = await fetchUsers(userIds);
const activeUsers = users.filter((user) => user.active);
return await Promise.all(
activeUsers.map((user) =>
this.runAction(sendWelcomeEmail({ to: user.email }), {
retry: { attempts: 5, backoffSeconds: 30 },
timeout: "10m",
}),
),
);
}
}
Same shape as the Python Workflow class. run() is the durable
entrypoint. this.runAction(...) is the explicit form for attaching
retry or timeout metadata to a call (and is what you should reach for
when the MVP rejects a plain inline call).
A few rules for the first pass:
- A workflow is an exported class extending
Workflow. - The durable entrypoint is
async run(...). - Workflow inputs go on
run(...), not onconstructor(...).
Starting from a route
The invocation site stays normal:
import { WelcomeEmailWorkflow } from "@/lib/workflows/welcome-email";
export async function POST(request: Request): Promise<Response> {
const { userIds } = await request.json();
const workflow = new WelcomeEmailWorkflow();
const result = await workflow.run(userIds);
return Response.json({ result });
}
workflow.run(...) no longer executes the body you authored - the
compiler replaced that body with code that submits the precompiled IR to
the bridge and waits for completion. The authored body is compiler
input, not runtime business logic.
withWaymark()
withWaymark(...) is the build-time integration point for Next.js. The
project opts in by wrapping its config:
// next.config.js
const { withWaymark } = require("@waymark/nextjs");
module.exports = withWaymark({
reactStrictMode: true,
});
What it does at build time:
- Registers a server-side loader for
.ts/.tsx/.jsmodules. - Parses raw source AST before Next.js strips the
// use actioncomments. - Discovers actions by
// use actionand workflows byclass ... extends Workflow. - Rewrites action modules so they self-register on import.
- Rewrites workflow
run()bodies to embed the compiled IR and submit it to the bridge instead of executing the authored body. - Maintains a generated bootstrap file at
.waymark/actions-bootstrap.mjsthat the worker loads at startup. - Invalidates rebuilds correctly when imported action modules change.
What it explicitly does not do: start the bridge, start workers, or
execute actions itself. Those are runtime concerns owned by
waymark-start-workers and the bridge binary.
What feels the same as Python
- Actions are the retryable, distributed work units.
- Workflows are the durable control-flow layer.
- The workflow definition is compiled once, not re-run during recovery.
- Retry and timeout policy attach to action calls, not the workflow as a whole.
- Parallel fan-out uses the language's native primitive. In TypeScript
that means
Promise.all(...); there's no Waymark-specific parallel API.
Initial restrictions
The first pass is intentionally narrow:
- Named
async functiondeclarations only - no arrow functions, no class methods marked as actions yet. - Workflow inputs on
run(...), not stored on the class. - The compiler only inspects
run(...)- helper methods on the class body are ignored, so push helpers into actions (or into plain functions called from inside actions).
These restrictions exist to keep the AST surface small. Expect them to loosen as the implementation lands; track the design doc for the latest.