Skip to content

Latest commit

 

History

History
323 lines (249 loc) · 8.36 KB

File metadata and controls

323 lines (249 loc) · 8.36 KB

Integration Guide

How to add the workflow engine to your SpacetimeDB game.

Overview

The workflow engine integrates into your game via Cargo dependencies. You add two crates, write workflows as sequential code, and register them with one macro. That's it.

Step 1: Add Dependencies

[dependencies]
workflow-core = { git = "https://github.com/perplexes/spacetime-workflow-engine" }
workflow-macros = { git = "https://github.com/perplexes/spacetime-workflow-engine" }
spacetimedb = "1.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"

Step 2: Define Your Workflows

Create workflow files using the #[workflow] macro:

// src/workflows/buff.rs

use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

#[derive(Timer)]
pub enum BuffTimer { Expire }

#[derive(Signal)]
pub enum BuffSignal {
    Dispel,
    Stack(u32),
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuffInit {
    pub effect_type: String,
    pub magnitude: i32,
    pub duration_secs: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BuffResult {
    pub effect_type: String,
    pub final_stacks: u32,
}

#[workflow]
fn buff(init: BuffInit) -> Result<BuffResult> {
    let mut stacks: u32 = 1;

    loop {
        select! {
            timer!(BuffTimer::Expire, init.duration_secs.secs()) => break,
            signal!(BuffSignal::Dispel) => break,
            signal!(BuffSignal::Stack(n)) => {
                stacks += n;
                continue
            },
        }.await;
    }

    Ok(BuffResult {
        effect_type: init.effect_type.clone(),
        final_stacks: stacks,
    })
}

Step 3: Install the Engine

In your module's lib.rs, call the install! macro with your workflow registrations:

// src/lib.rs

use spacetimedb::{reducer, ReducerContext};
use workflow_core::prelude::*;

mod workflows;

// Generate tables, reducers, and register workflows
workflow_macros::install! {
    "buff" => workflows::BuffWorkflow,
    "countdown" => workflows::CountdownWorkflow,
    "patrol" => workflows::PatrolWorkflow,
}

The macro generates:

  • Workflow table (stores workflow state)
  • WorkflowTimer table (scheduled timers)
  • LastWorkflowId table (for retrieving created workflow IDs)
  • WorkflowStatus enum
  • workflow_start reducer
  • workflow_signal reducer
  • workflow_cancel reducer
  • workflow_timer_fire reducer (called automatically by SpacetimeDB)
  • Automatic workflow registration that survives module updates

Step 4: Create Game Reducers

Add game-specific reducers that start workflows and send signals:

// src/lib.rs (continued)

#[reducer]
pub fn apply_buff(
    ctx: &ReducerContext,
    entity_id: u64,
    effect_type: String,
    magnitude: i32,
    duration_secs: u64,
) -> Result<(), String> {
    let init = workflows::BuffInit {
        effect_type,
        magnitude,
        duration_secs,
    };
    let data = serde_json::to_vec(&init).map_err(|e| e.to_string())?;

    workflow_start(ctx, "buff".to_string(), Some(entity_id), None, data)
}

#[reducer]
pub fn dispel_buff(ctx: &ReducerContext, workflow_id: u64) -> Result<(), String> {
    workflow_signal(ctx, workflow_id, "dispel".to_string(), vec![])
}

#[reducer]
pub fn stack_buff(ctx: &ReducerContext, workflow_id: u64, amount: u32) -> Result<(), String> {
    let payload = serde_json::to_vec(&amount).map_err(|e| e.to_string())?;
    workflow_signal(ctx, workflow_id, "stack".to_string(), payload)
}

Step 5: Query Workflows

Workflows are stored in the workflow table. Query them from your code:

use spacetimedb::Table;

// Find all active workflows for an entity
pub fn get_entity_workflows(ctx: &ReducerContext, entity_id: u64) -> Vec<Workflow> {
    ctx.db.workflow().iter()
        .filter(|w| w.entity_id == Some(entity_id) && w.is_active())
        .collect()
}

// Find a specific workflow
pub fn get_workflow(ctx: &ReducerContext, id: u64) -> Option<Workflow> {
    ctx.db.workflow().id().find(&id)
}

Client Integration

Clients can subscribe to workflow tables:

TypeScript

import { DbConnection } from './bindings';

const conn = await DbConnection.builder()
    .withUri('ws://localhost:3000')
    .withModuleName('my-game')
    .build();

// Subscribe to workflows for a specific entity
conn.subscriptionBuilder()
    .subscribe(`SELECT * FROM workflow WHERE entity_id = ${playerId}`);

// React to workflow changes
conn.db.workflow.onInsert((workflow) => {
    console.log(`New workflow: ${workflow.workflowType} - ${workflow.status}`);
});

conn.db.workflow.onUpdate((oldWorkflow, newWorkflow) => {
    console.log(`Workflow ${newWorkflow.id} status: ${newWorkflow.status}`);
});

// Start a workflow via reducer
await conn.reducers.applyBuff(entityId, "strength", 10, 60);

// Send a signal
await conn.reducers.dispelBuff(workflowId);

Complete Example

Here's a full module with the workflow engine:

// src/lib.rs
use spacetimedb::{reducer, ReducerContext};
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};

// ============================================
// BUFF WORKFLOW
// ============================================

#[derive(Timer)]
enum BuffTimer { Expire }

#[derive(Signal)]
enum BuffSignal { Dispel, Stack(u32) }

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BuffInit {
    effect_type: String,
    magnitude: i32,
    duration_secs: u64,
}

#[derive(Debug, Clone, Serialize, Deserialize)]
struct BuffResult {
    effect_type: String,
    final_stacks: u32,
}

#[workflow]
fn buff(init: BuffInit) -> Result<BuffResult> {
    let mut stacks: u32 = 1;

    loop {
        select! {
            timer!(BuffTimer::Expire, init.duration_secs.secs()) => break,
            signal!(BuffSignal::Dispel) => break,
            signal!(BuffSignal::Stack(n)) => {
                stacks += n;
                continue
            },
        }.await;
    }

    Ok(BuffResult {
        effect_type: init.effect_type.clone(),
        final_stacks: stacks,
    })
}

// ============================================
// INSTALL WORKFLOW ENGINE
// ============================================

workflow_macros::install! {
    "buff" => BuffWorkflow,
}

// ============================================
// GAME REDUCERS
// ============================================

#[reducer]
pub fn apply_buff(
    ctx: &ReducerContext,
    entity_id: u64,
    effect_type: String,
    magnitude: i32,
    duration_secs: u64,
) -> Result<(), String> {
    let init = BuffInit { effect_type, magnitude, duration_secs };
    let data = serde_json::to_vec(&init).map_err(|e| e.to_string())?;
    workflow_start(ctx, "buff".to_string(), Some(entity_id), None, data)
}

#[reducer]
pub fn dispel_buff(ctx: &ReducerContext, workflow_id: u64) -> Result<(), String> {
    workflow_signal(ctx, workflow_id, "dispel".to_string(), vec![])
}

#[reducer]
pub fn stack_buff(ctx: &ReducerContext, workflow_id: u64, amount: u32) -> Result<(), String> {
    let payload = serde_json::to_vec(&amount).map_err(|e| e.to_string())?;
    workflow_signal(ctx, workflow_id, "stack".to_string(), payload)
}

Tips

  1. Keep workflows focused — Each workflow handles one behavior. Don't combine unrelated logic.

  2. Use entity_id — Always pass the entity ID for workflows that affect game entities. Makes querying easy.

  3. Use correlation_id for grouping — Related workflows (e.g., all effects from one spell) can share a correlation ID.

  4. Workflows are for durable state — Use workflows for things that need to survive restarts. Don't use them for frame-by-frame logic.

  5. Transaction safety — Workflow handlers run in the same transaction as the trigger. If your handler fails, everything rolls back.

  6. Mutable variables need explicit typeslet mut count: u32 = 0; not let mut count = 0;

Project Structure

Recommended layout:

my-game/
├── src/
│   ├── lib.rs           # Main module, install! macro, game reducers
│   └── workflows/
│       ├── mod.rs       # Re-exports
│       ├── buff.rs      # Buff workflow
│       ├── patrol.rs    # Patrol workflow
│       └── combat.rs    # Combat workflow
├── Cargo.toml
└── tests/
    └── integration.ts   # TypeScript integration tests

Next Steps

API Reference — Complete API documentation → Examples — Real-world workflow patterns