Evok: Minimal Event-Driven Workflow Framework
A minimal framework for building event-driven workflows in TypeScript. Designed to coordinate logic across tasks, AI agents, or microservices, while remaining unopinionated and easy to extend.
Features:
- Written in TypeScript
- Builds to both modern ES modules and CommonJS formats
- Provides TypeScript type definitions
- ESLint for code linting
- Prettier for code formatting
- Vitest for testing
- Tsup for building
- Minimal dependencies
Installation
npm install @madooei/evok
Usage
Evok is a minimalist TypeScript library for building event-driven workflows using composable steps and declarative orchestration logic.
Core Concepts
State
The workflow operates on a shared state object (store). You define its shape using TypeScript types:
interface State {
data: {
greeting?: string;
};
}
WorkflowEvent
Steps emit events to signal transitions. Events contain a type
and optional payload
:
interface WorkflowEvent {
type: string;
payload?: Record<string, unknown>;
}
Step
Each step performs a unit of work:
interface Step<State> extends LifecycleHooks<State> {
id: string; // unique identifier for the step
// main logic to run, returning new state and optional events
run: (state: State) => Promise<{ state: State; events?: WorkflowEvent[] }>;
params?: Record<string, unknown>; // optional static config per step
}
Workflow
A workflow contains steps and orchestrates their execution via emitted events:
interface Workflow<State> extends Step<State> {
steps: Record<string, Step<State>>; // map of step IDs to Step instances for fast lookup
start: string; // ID of the starting step
// receives events and can return an array of step IDs to activate next
onEvent?: (event: WorkflowEvent, state: State) => Promise<string[]>;
}
Example Usage
import {
createWorkflow,
setLogLevel,
type Step,
type Workflow,
} from "@madooei/evok";
// 1. Define State
interface State {
data: {
greeting?: string;
};
}
// 2. Define Steps
const createGreetingStep: Step<State> = {
id: "createGreeting",
async run() {
const greeting = "Hello, World!";
return {
state: { data: { greeting } },
events: [{ type: "greeting:created", payload: greeting }],
};
},
onStart: async () => console.log("Starting createGreetingStep"),
onComplete: async () => console.log("Completed createGreetingStep"),
onError: async (error) =>
console.error("Error in createGreetingStep:", error),
};
const printGreetingStep: Step<State> = {
id: "printGreeting",
async run(state) {
const prefix = this.params?.prefix || "";
const postfix = this.params?.postfix || "";
console.log(prefix, state.data.greeting, postfix);
return { state };
},
params: { prefix: "πππ", postfix: "πππ" },
onStart: async () => console.log("Starting printGreetingStep"),
onComplete: async () => console.log("Completed printGreetingStep"),
onError: async (error) => console.error("Error in printGreetingStep:", error),
};
// 3. Define Workflow
const helloFlow = createWorkflow<State>({
id: "helloFlow",
start: "createGreeting",
steps: {
createGreeting: createGreetingStep,
printGreeting: printGreetingStep,
},
onEvent: async (event) => {
if (event.type === "greeting:created") {
return ["printGreeting"];
}
return [];
},
onStart: async () => console.log("Starting helloFlow"),
onComplete: async () => console.log("Completed helloFlow"),
onError: async (error) => console.error("Error in helloFlow:", error),
});
// 4. Execute
await helloFlow.run({ data: {} });
Logging
Evok includes built-in logging functionality:
import { setLogLevel } from "evok";
// Set log level to control verbosity
setLogLevel("trace"); // 'trace', 'info'
Cloning the Repository
To make your workflow more organized, itβs a good idea to clone this repository into a directory named evok-workspace
. This helps differentiate the workspace from the evok
located in the packages
directory.
git clone https://github.com/madooei/evok evok-workspace
cd evok-workspace
Repository Structure
packages
β Contains the primary package(s) for this repository (e.g.,evok
). Each package is self-contained and can be copied out and used independently.examples
β Contains examples of how to use the packages. Each example is a minimal, standalone project.playgrounds
β Contains demos of the dependencies of the primary package(s). Each playground is a minimal, standalone project.docs
β Contains various documentation for users and developers..github
β Contains GitHub-specific files, such as workflows and issue templates.
How to Use This Repo
- To work on a package, go to
packages/<package-name>
and follow its README. - To try an example, go to
examples/<example-name>
and follow its README. - To run the playground, go to
playground/<package-name>
and follow its README. - For documentation, see the
docs
folder.
Using a VSCode Multi-root Workspace
With Visual Studio Code, you can enhance your development experience by using a multi-root workspace to access packages, examples, and playgrounds simultaneously. This approach is more efficient than opening the root directory, or each package or example separately.
To set up a multi-root workspace:
- Open Visual Studio Code.
- Navigate to
File > Open Workspace from File...
. - Select the
evok.code-workspace
file located at the root of the repository. This action will open all specified folders in one workspace.
The evok.code-workspace
file can be customized to include different folders or settings. Hereβs a typical configuration:
{
"folders": [
{
"path": "packages/evok"
},
{
"path": "examples/agents"
},
{
"path": "playgrounds/empty"
}
],
"settings": {
// Add any workspace-specific settings here, for example:
"git.openRepositoryInParentFolders": "always"
}
}
Developing the Package
Change to the package directory and install dependencies:
cd packages/evok
npm install
- Read the Project Roadmap for project goals, status, evolution, and development guidelines.
- Read the Development Guide for detailed information on the package architecture, build configuration, and implementation patterns.
- Follow the Contributing Guide for contribution guidelines, coding standards, and best practices.
Package Management
When you are ready to publish your package:
npm run release
This single command will:
- Validate your code with the full validation pipeline
- Analyze commits to determine version bump
- Update package.json version and changelog
- Build the package
- Create and push git tag
- Create GitHub release
- Publish to NPM
[!TIP] For detailed information about package publishing, versioning, and local development workflows, see the NPM Package Management Guide.