Ablauf
Workflows

Supported Transport Types

Learn which data types are safe for workflow payloads, events, and SSE updates.

Supported Transport Types

All workflow data — input payloads, event payloads, and SSE updates — is serialized via SuperJSON for durable storage in SQLite and transport across Durable Object RPC boundaries. This means only data types that SuperJSON can handle are safe to use.

Ablauf enforces this at compile time via the t namespace and at runtime via schema validation at workflow registration.

The t Namespace

Instead of using z from Zod directly, workflow schemas use the constrained t namespace that only exposes SuperJSON-compatible types:

import { defineWorkflow } from '@der-ablauf/workflows';

const OrderWorkflow = defineWorkflow((t) => ({
	type: 'process-order',
	input: t.object({
		orderId: t.string(),
		items: t.array(t.object({ name: t.string(), price: t.number() })),
		placedAt: t.date(),
		metadata: t.map(t.string(), t.unknown()),
	}),
	events: {
		payment: t.object({ amount: t.number(), transactionId: t.string() }),
	},
	run: async (step, payload) => {
		// payload is fully typed
		return { processed: true };
	},
}));

For BaseWorkflow, import t directly:

import { BaseWorkflow, t } from '@der-ablauf/workflows';

class MyWorkflow extends BaseWorkflow<...> {
	static inputSchema = t.object({ name: t.string() });
}

Supported Types

Primitives

MethodTypeScript TypeSuperJSON
t.string()stringNative JSON
t.number()numberNative JSON
t.boolean()booleanNative JSON
t.null()nullNative JSON
t.undefined()undefinedSuperJSON handles
t.bigint()bigintSuperJSON handles

Rich Types

MethodTypeScript TypeSuperJSON
t.date()DateSuperJSON handles
t.map(key, value)Map<K, V>SuperJSON handles
t.set(value)Set<T>SuperJSON handles
t.url()URLSuperJSON handles

Structural

MethodTypeScript Type
t.object(shape){ [key]: T }
t.array(item)T[]
t.tuple(items)[A, B, ...]
t.record(key, value)Record<K, V>

Combinators

MethodDescription
t.literal(value)Exact value match
t.enum(values)String enum
t.nativeEnum(enum)TypeScript enum
t.union(options)Union of types
t.discriminatedUnion(key, options)Tagged union
t.intersection(a, b)Intersection
t.optional()Optional wrapper
t.nullable()Nullable wrapper
t.lazy(fn)Recursive/deferred schemas

All Zod modifiers like .default(), .transform(), .refine(), .pipe() work as expected on t.* schemas.

Escape Hatches

t.any() and t.unknown() bypass transport type safety. The actual runtime value must be SuperJSON-compatible or serialization will fail silently. Prefer explicit types whenever possible.

These are available for cases where you genuinely need flexible schemas, but use them sparingly:

defineWorkflow((t) => ({
	type: 'flexible',
	input: t.object({
		data: t.unknown(), // ⚠️ Caller must ensure SuperJSON compatibility
	}),
	run: async (step, payload) => { ... },
}));

Both t.any() and t.unknown() emit a console.warn at schema creation time to remind you of the risk.

Excluded Types

These Zod types are not available on t and will cause a compile error:

Zod TypeReason
z.function()Functions are not serializable
z.promise()Promises are not serializable
z.instanceof()Arbitrary classes are not serializable (use t.url() for URL)
z.symbol()Symbols are not supported by SuperJSON
z.void()Not meaningful for transport data
z.never()Not meaningful for transport data

If you attempt to bypass t and smuggle an unsupported type, the runtime validation will throw an InvalidSchemaError at workflow registration time.

Using Existing Zod Schemas

If you have Zod schemas defined elsewhere in your app, wrap them with serializable() to validate and brand them:

import { serializable, BaseWorkflow } from '@der-ablauf/workflows';
import { z } from 'zod';

// Shared schema from your app
const userSchema = z.object({ name: z.string(), email: z.string() });

class UserWorkflow extends BaseWorkflow<...> {
	static inputSchema = serializable(userSchema);
	// Throws InvalidSchemaError if userSchema uses unsupported types
}

Migration from z to t

If you have existing workflows using z directly:

Functional API — wrap in callback, replace z. with t.:

// Before
defineWorkflow({
	type: 'my-workflow',
	input: z.object({ name: z.string() }),
	run: async (step, payload) => { ... },
});

// After
defineWorkflow((t) => ({
	type: 'my-workflow',
	input: t.object({ name: t.string() }),
	run: async (step, payload) => { ... },
}));

Class-based — import t, replace z. with t. in static properties:

// Before
import { z } from 'zod';
static inputSchema = z.object({ name: z.string() });

// After
import { t } from '@der-ablauf/workflows';
static inputSchema = t.object({ name: t.string() });

On this page