Your First Workflow
This guide walks through building a workflow that fetches a user and their posts, with typed error handling.
Define your operations
Section titled “Define your operations”Operations return AsyncResult<T, E> - either ok(value) or err(error):
import { ok, err, type AsyncResult } from 'awaitly';
type User = { id: string; name: string };type Post = { id: number; title: string };
const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => { if (id === '1') { return ok({ id: '1', name: 'Alice' }); } return err('NOT_FOUND');};
const fetchPosts = async (userId: string): AsyncResult<Post[], 'FETCH_ERROR'> => { return ok([ { id: 1, title: 'Hello World' }, { id: 2, title: 'Second Post' }, ]);};Create the workflow
Section titled “Create the workflow”Pass your operations to createWorkflow. Error types are inferred automatically:
import { createWorkflow } from 'awaitly/workflow';
const loadUserData = createWorkflow('workflow', { fetchUser, fetchPosts });Adding workflow options
Section titled “Adding workflow options”Options like caching, events, and resume state are passed to createWorkflow, or per-run via workflow.run(fn, config):
// Correct: Options at creationconst workflow = createWorkflow('workflow', { fetchUser }, { cache: new Map(), onEvent: (e) => console.log(e)});await workflow.run(async ({ step }) => { ... });
// Per-run options: pass config as second argument to .run()await workflow.run(async ({ step }) => { ... }, { signal: controller.signal });Run it
Section titled “Run it”Use step() to execute operations. Prefer step('id', fn) so step names appear in statically generated docs and diagrams. If any step fails, the workflow exits early:
const result = await loadUserData.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1')); const posts = await step('fetchPosts', () => deps.fetchPosts(user.id)); return { user, posts };});Handle the result
Section titled “Handle the result”Check result.ok to determine success or failure:
if (result.ok) { console.log(result.value.user.name); console.log(result.value.posts.length, 'posts');} else { // TypeScript knows: result.error is 'NOT_FOUND' | 'FETCH_ERROR' | UnexpectedError switch (result.error) { case 'NOT_FOUND': console.log('User not found'); break; case 'FETCH_ERROR': console.log('Failed to fetch posts'); break; default: // UnexpectedError - something threw an exception console.log('Unexpected error:', result.error); }}Complete example
Section titled “Complete example”import { ok, err, type AsyncResult } from 'awaitly';import { createWorkflow } from 'awaitly/workflow';
type User = { id: string; name: string };type Post = { id: number; title: string };
const fetchUser = async (id: string): AsyncResult<User, 'NOT_FOUND'> => id === '1' ? ok({ id: '1', name: 'Alice' }) : err('NOT_FOUND');
const fetchPosts = async (userId: string): AsyncResult<Post[], 'FETCH_ERROR'> => ok([{ id: 1, title: 'Hello World' }]);
const loadUserData = createWorkflow('workflow', { fetchUser, fetchPosts });
const result = await loadUserData.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('1')); const posts = await step('fetchPosts', () => deps.fetchPosts(user.id)); return { user, posts };});
if (result.ok) { console.log(`${result.value.user.name} has ${result.value.posts.length} posts`);}What happens on error?
Section titled “What happens on error?”Change fetchUser('1') to fetchUser('999'):
const result = await loadUserData.run(async ({ step, deps }) => { const user = await step('fetchUser', () => deps.fetchUser('999')); // Returns err('NOT_FOUND') // This line never runs const posts = await step('fetchPosts', () => deps.fetchPosts(user.id)); return { user, posts };});
console.log(result.ok); // falseconsole.log(result.error); // 'NOT_FOUND'The workflow exits at the first error. No need for try/catch or manual error checking.