evok

Evok is a minimal framework for building event-driven workflows in TypeScript.

View on GitHub

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:

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

How to Use This Repo

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:

  1. Open Visual Studio Code.
  2. Navigate to File > Open Workspace from File....
  3. 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

Package Management

When you are ready to publish your package:

npm run release

This single command will:

[!TIP] For detailed information about package publishing, versioning, and local development workflows, see the NPM Package Management Guide.