Skip to content

Conversation

@alex-holovach
Copy link
Contributor

I was curious about how workflows could be used on the client side and decided to explore a Workers-based implementation that allows running workflows as durable, long-running tasks entirely in the browser.

As a proof of concept, I built an AI chat that runs completely locally using WebLLM - no backend required. The LLM inference runs in a Web Worker with the same 'use workflow' syntax we use on the server.

local-chat-demo.mp4

How It Works

┌─────────────────────────────────────────────────────────────┐
│                      Browser (Main Thread)                   │
│  ┌─────────────────┐      ┌─────────────────────────────┐   │
│  │   React App     │      │   Transformed Workflow      │   │
│  │                 │ ───► │   chatWithLLM()             │   │
│  │  'use client'   │      │   → sends message to worker │   │
│  └─────────────────┘      └──────────────┬──────────────┘   │
└──────────────────────────────────────────┼──────────────────┘
                                           │
                                           ▼
┌─────────────────────────────────────────────────────────────┐
│                      Web Worker                              │
│  ┌─────────────────────────────────────────────────────┐    │
│  │              Browser World                           │    │
│  │  ┌─────────────┐  ┌─────────────┐  ┌─────────────┐  │    │
│  │  │ Turso WASM  │  │   Queue     │  │  Workflow   │  │    │
│  │  │ (SQLite)    │  │  Processor  │  │  Executor   │  │    │
│  │  └─────────────┘  └─────────────┘  └─────────────┘  │    │
│  └─────────────────────────────────────────────────────┘    │
│                            │                                 │
│                            ▼                                 │
│  ┌─────────────────────────────────────────────────────┐    │
│  │   'use workflow' function body executes here        │    │
│  │   → WebLLM generates response using WebGPU          │    │
│  └─────────────────────────────────────────────────────┘    │
└─────────────────────────────────────────────────────────────┘

Key Features

  • Same syntax - Use 'use workflow' directive just like server-side workflows
  • Runs in Web Worker - Heavy computation doesn't block the UI
  • Durable storage - Turso WASM (SQLite) persists workflow state in the browser
  • WebGPU acceleration - LLM inference uses GPU when available

Example

// app/workflows/browser/chat.ts
export async function chatWithLLM(messages) {
  'use workflow';
  
  const llm = await initWebLLM();
  return await llm.chat.completions.create({ messages });
}
// app/chat/page.tsx
'use client';

import { chatWithLLM } from '@/app/workflows/browser/chat';

export default function ChatPage() {
  const handleSubmit = async (message) => {
    // Runs in browser worker, not main thread
    const response = await chatWithLLM(messages);
  };
  
  return <ChatUI onSubmit={handleSubmit} />;
}

How Compilation Works

The SWC plugin detects 'use workflow' in files matching the browser workflow pattern and transforms the code:

Source (what you write):

export async function chatWithLLM(messages) {
  'use workflow';
  const llm = await initWebLLM();
  return await llm.chat.completions.create({ messages });
}

Client bundle (main thread):

// Function body replaced with worker communication
export async function chatWithLLM(messages) {
  return __browserWorkflowClient.run("chatWithLLM", [messages]);
}

Worker bundle (Web Worker):

// Original function body preserved and registered
workflowRegistry.set("chatWithLLM", async function(messages) {
  const llm = await initWebLLM();
  return await llm.chat.completions.create({ messages });
});

@changeset-bot
Copy link

changeset-bot bot commented Dec 1, 2025

⚠️ No Changeset found

Latest commit: ddb386d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@vercel
Copy link
Contributor

vercel bot commented Dec 1, 2025

@alex-holovach is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

@alex-holovach alex-holovach marked this pull request as draft December 1, 2025 23:44
@socket-security
Copy link

@alex-holovach
Copy link
Contributor Author

This is an early experiment, needs a lot of refactoring before proper review. Just wanted to share the idea and get some thoughts on whether client-side workflows via Web Workers makes sense.

cc @pranaygp

@TooTallNate
Copy link
Member

Pretty cool!

I'm curious, how do you (plan on) addressing the determinism features that the VM provides when executing the workflow function? i.e. seeding Math.random(), fixed Date.now(), etc.

@alex-holovach
Copy link
Contributor Author

Pretty cool!

I'm curious, how do you (plan on) addressing the determinism features that the VM provides when executing the workflow function? i.e. seeding Math.random(), fixed Date.now(), etc.

Great question. I was wondering do we really need determinism in client side workflows? Current implementation uses checkpoint-based resumption (steps check DB for cached results) rather than event replay

Copy link
Collaborator

pranaygp commented Dec 2, 2025

haven’t looked at code yet, just your description, but I do think we need determinism in workflow. ideally workflows are just portable and the world can swap underneath. event replay is also gonna matter for more features like versioning. checkpoint could be an optimization on top of event replay to allow us to skip event replay when a checkpoint is available. also open sockets wouldn’t cleanly retain across checkpoints if you allow the workflow context to “run anything”

@alex-holovach
Copy link
Contributor Author

yeah that makes a lot of sense, i'll explore some options how to make it deterministic

@karthikscale3
Copy link
Contributor

karthikscale3 commented Dec 2, 2025

Super cool! I have been hacking on something similar as well. But, I have a slightly different implementation.

  • QuickJS WASM: Used only for deterministic random number generation (via vmContext.globalThis.Math.random()) to generate correlation IDs. The VM context is created but not used to execute workflow code. cc @TooTallNate
  • Workflow runtime: Executes in the browser's main JavaScript context using AsyncFunction, not inside QuickJS.
  • Steps: Also run in the browser's main context via registered step functions.

I will share my setup soon.

@karthikscale3
Copy link
Contributor

karthikscale3 commented Dec 2, 2025

Also, I had the exact thought process - using the browser world with WebLLM to build purely client side workflow builders. You can also take it one step further by storing the application layer metadata in indexDB persistence

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants