Skip to content

Cryrivers/motif-ts

Repository files navigation

motif-ts

npm version License Repo Size

Dead Simple. Fully Typed. Effortlessly Orchestrated.

motif-ts is a type-safe workflow orchestrator for TypeScript. It allows you to build complex state machines and workflows with fully typed steps, dynamic edge conditions, and time-travel debugging capabilities. It is designed to be framework-agnostic while providing first-class support for React.

Packages

This repository is a monorepo containing the following packages:

Key Features

  • Type Safety: Built with Zod for runtime validation and static type inference. Inputs, outputs, and configurations are all fully typed.
  • Visualizable Workflows: The structure of your workflow is declarative, making it easy to visualize as a graph (DAG or cyclic).
  • Time-Travel Debugging: Seamless integration with Redux DevTools. Inspect every state change, jump back in time, and replay actions.
  • Expression Engine: Use safe, dynamic expressions for conditional edges (e.g., input.value > 10) and data transformations between steps.
  • Framework Agnostic: Core logic is pure TypeScript. Adapters for frameworks like React are provided, but you can use it anywhere.

Usage Example

Here is a quick example of how to define a simple workflow, connect it, and use it.

1. Define Steps

Use the step helper to define atomic units of work.

  • Input/Output/Config Schemas: Define validation and types for step data.
  • apiSchema (Optional): Define the API shape and use .describe() to provide context for AI agents.
  • createStore: define local, reactive state for the step (uses Zustand).
  • Lifecycle & Effects: Use transitionIn, transitionOut, and effect to orchestrate side effects.
import { step } from '@motif-ts/core';
import z from 'zod';
import { type StateCreator } from 'zustand/vanilla';

// A step that collects an email
const CollectEmail = step(
  {
    kind: 'CollectEmail',
    outputSchema: z.object({ email: z.string().email() }),
    // Optional: Add descriptions for AI tools
    apiSchema: z.object({
      submit: z.function().describe('Submits the collected email address'),
    }),
  },
  ({ next }) => ({
    submit: (email: string) => next({ email }),
  }),
);

// Define local state for verification (timer, status)
interface VerifyState {
  isChecking: boolean;
  timeLeft: number;
  decrement: () => void;
  setChecking: (checking: boolean) => void;
}

const verifyStore: StateCreator<VerifyState> = (set) => ({
  isChecking: false,
  timeLeft: 5, // 5s to verify
  decrement: () => set((s) => ({ timeLeft: Math.max(0, s.timeLeft - 1) })),
  setChecking: (isChecking) => set({ isChecking }),
});

// A step that verifies the email with a countdown and mock async check
const VerifyEmail = step(
  {
    kind: 'VerifyEmail',
    inputSchema: z.object({ email: z.string() }),
    outputSchema: z.object({ verified: z.boolean() }),
    apiSchema: z.object({
      timeLeft: z.number().describe('Seconds remaining for verification'),
      isChecking: z.boolean(),
      email: z.string(),
      verify: z.function().describe('Triggers the verification process'),
    }),
    createStore: verifyStore,
  },
  ({ input, next, store, transitionIn, transitionOut, effect }) => {
    // Lifecycle: Start countdown on entry, clean up on exit
    transitionIn(() => {
      const interval = setInterval(() => store.decrement(), 1000);
      return () => clearInterval(interval);
    });

    // Effect: React to state changes (e.g. timeout)
    effect(() => {
      if (store.timeLeft === 0) {
        // Handle timeout (e.g., disable UI or auto-transition)
        console.log('Verification timed out for:', input.email);
      }
    }, [store.timeLeft]);

    // Return the API exposed to the UI
    return {
      timeLeft: store.timeLeft,
      isChecking: store.isChecking,
      email: input.email,
      verify: async () => {
        if (store.timeLeft === 0) return;

        store.setChecking(true);
        // Simulate async verification API
        await new Promise((resolve) => setTimeout(resolve, 1000));

        const isValid = input.email.endsWith('@company.com');
        store.setChecking(false);
        next({ verified: isValid });
      },
    };
  },
);

2. Create and Connect Workflow

Combine steps into a workflow and define the flow.

import { workflow } from '@motif-ts/core';
import { devtools } from '@motif-ts/middleware';

const orchestrator = workflow([CollectEmail, VerifyEmail]);

// Instantiate steps with unique names
const collect = CollectEmail('collect');
const verify = VerifyEmail('verify');

// Register and connect
orchestrator.register([collect, verify]);
orchestrator.connect(collect, verify);

// Enhance with DevTools middleware (optional)
const app = devtools(orchestrator);

// Start the workflow
app.start(collect);

3. Use in React

Use the provided hooks to consume the workflow state in your React components.

import { useWorkflow } from '@motif-ts/react';

function App() {
  const current = useWorkflow(app);

  if (current.kind === 'CollectEmail') {
    return <button onClick={() => current.state.submit('user@company.com')}>Submit Email</button>;
  }

  if (current.kind === 'VerifyEmail') {
    const { timeLeft, isChecking, verify, email } = current.state;
    return (
      <div>
        <p>Verifying: {email}</p>
        <p>Time remaining: {timeLeft}s</p>
        <button disabled={isChecking || timeLeft === 0} onClick={() => verify()}>
          {isChecking ? 'Verifying...' : 'Verify Code'}
        </button>
      </div>
    );
  }

  return <div>Done</div>;
}

4. Use in Vue

Use useWorkflow to consume the workflow state in your Vue components.

<script setup lang="ts">
import { useWorkflow } from '@motif-ts/vue';

import { app } from './workflow'; // Assuming workflow from above is exported as 'app'

const current = useWorkflow(app);
</script>

<template>
  <div v-if="current.kind === 'CollectEmail'">
    <button @click="current.state.submit('user@company.com')">Submit Email</button>
  </div>

  <div v-else-if="current.kind === 'VerifyEmail'">
    <p>Verifying: {{ current.state.email }}</p>
    <p>Time remaining: {{ current.state.timeLeft }}s</p>
    <button :disabled="current.state.isChecking || current.state.timeLeft === 0" @click="current.state.verify()">
      {{ current.state.isChecking ? 'Verifying...' : 'Verify Code' }}
    </button>
  </div>

  <div v-else>Done</div>
</template>

5. Use in Svelte

Use createWorkflowStore to create a reactive Svelte store.

<script lang="ts">
  import { createWorkflowStore } from "@motif-ts/svelte";
  import { app } from "./workflow"; // Assuming workflow from above is exported as 'app'

  const current = createWorkflowStore(app);
</script>

{#if $current.kind === "CollectEmail"}
  <button on:click={() => $current.state.submit("user@company.com")}>
    Submit Email
  </button>
{:else if $current.kind === "VerifyEmail"}
  <div>
    <p>Verifying: {$current.state.email}</p>
    <p>Time remaining: {$current.state.timeLeft}s</p>
    <button
      disabled={$current.state.isChecking || $current.state.timeLeft === 0}
      on:click={() => $current.state.verify()}
    >
      {$current.state.isChecking ? "Verifying..." : "Verify Code"}
    </button>
  </div>
{:else}
  <div>Done</div>
{/if}

Development

This project uses pnpm and turbo.

# Install dependencies
pnpm install

# Build all packages
pnpm build

# Run tests
pnpm test

# Run the website documentation locally
pnpm --filter website dev

License

MIT

About

Dead Simple. Fully Typed. Effortlessly Orchestrated.

Topics

Resources

Stars

Watchers

Forks

Contributors

Languages