Production-ready workflows demonstrating common game patterns.
| Example | Pattern | Key Techniques |
|---|---|---|
| Buff | Timed effects | Loop + select, signal payloads, stacking |
| Countdown | Periodic updates | Loop + timer, mutable state |
| Conditional | Branching | If/else with await |
| Parent-Child | Hierarchical | For loop + spawn |
| Patrol | Looping behavior | Waypoint iteration, threat response |
| Production | Economic cycle | State machine, pause/resume |
| Combat | Turn-based | Action timeout, select with break/continue |
| Respawn | Uninterruptible | Simple timer, no signals |
| Day/Night | Global singleton | Infinite loop, explicit stop, correlation_id |
| Auction | Timed bidding | Bid signals, buyout, timeout |
| Trade | Multi-party | Both parties accept, timeout |
| Quest | Objectives | Progress tracking, abandon/fail |
Use case: Timed status effect with stacking and early termination.
┌───────┐
│ START │
└───┬───┘
│
▼
┌────────┐ expire timer ┌─────────┐
│ ACTIVE │──────────────────▶│ EXPIRED │──▶ Complete
└───┬────┘ └─────────┘
│
│ stack signal (continue loop)
│ dispel signal (break)
▼
┌────────┐
│ ACTIVE │ (timer restarted)
└────────┘
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};
#[derive(Timer)]
pub enum BuffTimer { Expire }
#[derive(Signal)]
pub enum BuffSignal {
Dispel,
Stack(u32), // Payload auto-deserialized
}
#[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 was_dispelled: bool,
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(),
was_dispelled: false,
final_stacks: stacks,
})
}| Signal | Payload | Effect |
|---|---|---|
dispel |
— | End immediately |
stack |
amount (u32) | Add stacks, continue |
Use case: Timer that ticks down, can be reset or cancelled.
┌───────┐
│ START │
└───┬───┘
│ count = init.count
▼
┌─────────────┐
│ TICKING │◀─────────────────┐
└──────┬──────┘ │
│ │
┌──────┴──────┬─────────────┐ │
│ tick timer │ reset signal │ │
▼ ▼ │ │
count -= 1 count = new continue
│ │
└───────────────────────────────┘
│
count == 0 ?
│
▼
┌───────────┐
│ COMPLETED │
└───────────┘
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};
#[derive(Timer)]
pub enum CountdownTimer { Tick }
#[derive(Signal)]
pub enum CountdownSignal {
Cancel,
Reset(u32), // New count value
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CountdownInit { pub count: u32 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CountdownResult {
pub completed: bool,
pub final_count: u32,
}
#[workflow]
fn countdown(init: CountdownInit) -> Result<CountdownResult> {
let mut count: u32 = init.count;
loop {
select! {
timer!(CountdownTimer::Tick, 1.secs()) => {
count = count.saturating_sub(1);
if count == 0 { break }
continue
},
signal!(CountdownSignal::Cancel) => break,
signal!(CountdownSignal::Reset(new_count)) => {
count = new_count;
continue
},
}.await;
}
Ok(CountdownResult {
completed: count == 0,
final_count: count,
})
}| Signal | Payload | Effect |
|---|---|---|
cancel |
— | Stop immediately |
reset |
count (u32) | Reset to new value |
Use case: Different behavior based on initialization data.
┌───────────────┐
│ START │
└───────┬───────┘
│
┌───────┴───────┐
│ use_short? │
▼ ▼
┌───────┐ ┌───────┐
│ SHORT │ │ LONG │
│ timer │ │ timer │
│ (1s) │ │ (2s) │
└───┬───┘ └───┬───┘
│ │
│ value += 10 │ value += 20
│ │
└──────┬───────┘
▼
┌───────────┐
│ COMPLETED │
└───────────┘
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer};
#[derive(Timer)]
pub enum ConditionalTimer {
Short,
Long,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionalInit {
pub use_short_timer: bool,
pub value: i32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ConditionalResult {
pub used_short: bool,
pub final_value: i32,
}
#[workflow]
fn conditional_test(init: ConditionalInit) -> Result<ConditionalResult> {
let mut value: i32 = init.value;
// Conditional with await - each branch waits for a different timer
if init.use_short_timer {
timer!(ConditionalTimer::Short, 1.secs()).await;
value += 10;
} else {
timer!(ConditionalTimer::Long, 2.secs()).await;
value += 20;
}
Ok(ConditionalResult {
used_short: init.use_short_timer,
final_value: value,
})
}Use case: Workflow that spawns multiple child workflows and aggregates results.
┌─────────────┐
│ PARENT │
└──────┬──────┘
│
for i in 0..n {
│
▼
┌─────────────┐
│ spawn child │
│ wait result │
└──────┬──────┘
│
completed_count += 1
│
} │
▼
┌─────────────┐
│ COMPLETED │
│ all_completed │
└─────────────┘
use workflow_core::prelude::*;
use workflow_macros::workflow;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParentInit { pub num_children: u32 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ParentResult {
pub children_spawned: u32,
pub all_completed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChildInit { pub delay_secs: u64 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChildResult { pub completed: bool }
#[workflow]
fn parent_workflow(init: ParentInit) -> Result<ParentResult> {
let mut completed_count: u32 = 0;
// For loop with await - spawns children sequentially
for _i in 0..init.num_children as usize {
let child_init = ChildInit { delay_secs: 1 };
let child_result: ChildResult = spawn!("child", child_init).await;
if child_result.completed {
completed_count += 1;
}
}
Ok(ParentResult {
children_spawned: init.num_children,
all_completed: completed_count == init.num_children,
})
}Use case: NPC walks between waypoints, responds to threats.
┌──────────────────────────────────────────────────────────┐
│ │
▼ │
┌───────┐ │
│ IDLE │ │
└───┬───┘ │
│ start timer │
▼ │
┌─────────┐ arrival timer ┌─────────────┐ │
│ WALKING │──────────────────▶│ AT_WAYPOINT │ │
└────┬────┘ └──────┬──────┘ │
│ │ wait timer │
│ threat_detected ▼ │
│ signal ┌─────────────────┐ │
│ │ next waypoint │────────────────┘
▼ └─────────────────┘
┌─────────┐
│ COMBAT │───▶ spawns CombatWorkflow
└────┬────┘
│ child completes
▼
┌───────────────┐
│ RESUME PATROL │
└───────────────┘
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};
#[derive(Timer)]
pub enum PatrolTimer {
StartPatrol,
Arrival,
Wait,
}
#[derive(Signal)]
pub enum PatrolSignal {
ThreatDetected(u64), // enemy_id
StandDown,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatrolInit {
pub waypoint_count: u32,
pub walk_time_secs: u64,
pub wait_time_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PatrolResult {
pub waypoints_visited: u32,
pub threats_engaged: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatInit { pub enemy_id: u64 }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatResult { pub victory: bool }
#[workflow]
fn patrol(init: PatrolInit) -> Result<PatrolResult> {
let mut waypoints_visited: u32 = 0;
let mut threats_engaged: u32 = 0;
// Initial delay before starting patrol
timer!(PatrolTimer::StartPatrol, 2.secs()).await;
loop {
// Walk to next waypoint
timer!(PatrolTimer::Arrival, init.walk_time_secs.secs()).await;
waypoints_visited += 1;
// Wait at waypoint, but respond to threats
select! {
timer!(PatrolTimer::Wait, init.wait_time_secs.secs()) => {
// Check if we've completed the patrol
if waypoints_visited >= init.waypoint_count {
break
}
continue
},
signal!(PatrolSignal::ThreatDetected(enemy_id)) => {
// Spawn combat workflow and wait for result
let combat_init = CombatInit { enemy_id };
let _result: CombatResult = spawn!("combat", combat_init).await;
threats_engaged += 1;
// Resume patrol after combat
continue
},
signal!(PatrolSignal::StandDown) => break,
}.await;
}
Ok(PatrolResult {
waypoints_visited,
threats_engaged,
})
}| Signal | Payload | Effect |
|---|---|---|
threat_detected |
enemy ID (u64) | Spawn combat, then resume |
stand_down |
— | Return home, end patrol |
Use case: Factory that converts inputs to outputs on a cycle.
┌─────────────────┐
│ AWAITING_INPUTS │◀──────────────────────────┐
└────────┬────────┘ │
│ inputs available │
▼ │
┌────────────────┐ │
│ PRODUCING │ │
└────────┬───────┘ │
│ cycle_complete timer │
▼ │
┌─────────────────┐ │
│ deposit output │──────────────────────────┘
│ cycles += 1 │
└─────────────────┘
Any state + "halt" signal ──▶ HALTED
HALTED + "resume" signal ──▶ previous state
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};
#[derive(Timer)]
pub enum ProductionTimer { CycleComplete }
#[derive(Signal)]
pub enum ProductionSignal {
Halt,
Resume,
Stop,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductionInit {
pub blueprint_id: String,
pub cycle_duration_secs: u64,
pub max_cycles: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProductionResult {
pub cycles_completed: u32,
pub was_halted: bool,
}
#[workflow]
fn production(init: ProductionInit) -> Result<ProductionResult> {
let mut cycles_completed: u32 = 0;
let mut halted: bool = false;
loop {
// Check for halt before each cycle
if halted {
select! {
signal!(ProductionSignal::Resume) => {
halted = false;
continue
},
signal!(ProductionSignal::Stop) => break,
}.await;
}
// Production cycle
select! {
timer!(ProductionTimer::CycleComplete, init.cycle_duration_secs.secs()) => {
cycles_completed += 1;
// Check if we've reached max cycles
if cycles_completed >= init.max_cycles {
break
}
continue
},
signal!(ProductionSignal::Halt) => {
halted = true;
continue
},
signal!(ProductionSignal::Stop) => break,
}.await;
}
Ok(ProductionResult {
cycles_completed,
was_halted: halted,
})
}| Signal | Payload | Effect |
|---|---|---|
halt |
— | Pause production |
resume |
— | Continue production |
stop |
— | End workflow |
Use case: Turn-based combat with action submission and timeout.
┌─────────────────────────────────────────────────────────────┐
│ COMBAT WORKFLOW │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────┐ │
│ │ START │ Initialize round counter │
│ └───┬───┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ AWAITING_ACTION │◀─────────────────────┐ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ┌──────┼──────────────┐ │ │
│ │ │ │ │ │
│ timeout submit_action flee │ │
│ │ │ │ │ │
│ ▼ ▼ ▼ │ │
│ round++ round++ ┌──────────┐ │ │
│ │ │ │ COMPLETE │ │ │
│ │ │ │ (fled) │ │ │
│ │ │ └──────────┘ │ │
│ │ │ │ │
│ └──────┴───────────────────────────────┘ │
│ (if round < max_rounds) │
│ │
│ (if round >= max_rounds) │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ COMPLETE │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};
#[derive(Timer)]
pub enum CombatTimer {
ActionTimeout,
}
#[derive(Signal)]
pub enum CombatSignal {
SubmitAction(CombatAction),
Flee, // Unit variant - no payload
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatAction {
pub actor_id: u64,
pub action_type: String,
pub target_id: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatInit {
pub attacker_id: u64,
pub defender_id: u64,
pub action_timeout_secs: u64,
pub max_rounds: u32,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CombatResult {
pub winner_id: Option<u64>,
pub rounds_fought: u32,
pub ended_by_flee: bool,
}
#[workflow]
fn combat(init: CombatInit) -> Result<CombatResult> {
let mut round: u32 = 0;
let mut ended_by_flee: bool = false;
loop {
round += 1;
select! {
timer!(CombatTimer::ActionTimeout, init.action_timeout_secs.secs()) => {
if round >= init.max_rounds {
break
}
continue
},
signal!(CombatSignal::SubmitAction(_action)) => {
if round >= init.max_rounds {
break
}
continue
},
signal!(CombatSignal::Flee) => {
ended_by_flee = true;
break
},
}.await;
}
Ok(CombatResult {
winner_id: if ended_by_flee { Some(init.defender_id) } else { None },
rounds_fought: round,
ended_by_flee,
})
}| Signal | Payload | Effect |
|---|---|---|
submit_action |
CombatAction | Record action, continue to next round |
flee |
— | End combat immediately |
Use case: Simple uninterruptible timer to respawn an entity after death.
┌────────┐ ┌─────────┐ ┌──────────┐
│ START │───schedule────▶│ WAITING │───timer:spawn─▶│ COMPLETE │
└────────┘ └─────────┘ └────┬─────┘
│
┌──────────────────┘
│ Side effects:
│ • Create entity
│ • Start AI workflow
│ • Initialize inventory
└────────────────────────
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer};
#[derive(Timer)]
pub enum RespawnTimer { Spawn }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RespawnInit {
pub entity_template: String,
pub spawn_location_id: u64,
pub respawn_delay_secs: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RespawnResult {
pub spawned_entity_id: u64,
}
// Simple uninterruptible workflow - no signals, just wait and spawn
#[workflow]
fn respawn(init: RespawnInit) -> Result<RespawnResult> {
// Wait for respawn delay
timer!(RespawnTimer::Spawn, init.respawn_delay_secs.secs()).await;
// Spawn the entity via reducer (returns new entity ID via table)
reducer!(spawn_entity(
init.entity_template.clone(),
init.spawn_location_id
));
// In real code, you'd look up the spawned entity ID
// For demo, using placeholder
Ok(RespawnResult {
spawned_entity_id: 0,
})
}Key pattern: No select!, no signals - purely timer-driven. The entity will respawn no matter what. This is the simplest workflow pattern.
Use case: Global world state that cycles between time-of-day phases.
┌─────────────────────────────────────────────────────────────┐
│ DAY/NIGHT CYCLE │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────┐ ┌─────┐ ┌──────┐ ┌───────┐ │
│ │ DAWN │──▶│ DAY │──▶│ DUSK │──▶│ NIGHT │──┐ │
│ └──────┘ └─────┘ └──────┘ └───────┘ │ │
│ ▲ │ │
│ └──────────────────────────────────────┘ │
│ │
│ Transitions: timer:phase_end OR signal:skip_phase │
│ Exit: signal:stop │
│ │
└─────────────────────────────────────────────────────────────┘
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};
#[derive(Timer)]
pub enum DayNightTimer {
PhaseEnd,
}
#[derive(Signal)]
pub enum DayNightSignal {
SkipPhase, // Jump to next phase immediately
Stop, // End the cycle
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
pub enum TimeOfDay { Dawn, Day, Dusk, Night }
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DayNightInit {
pub phase_duration_secs: u64,
pub starting_phase: TimeOfDay,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DayNightResult {
pub cycles_completed: u32,
pub final_phase: TimeOfDay,
pub stopped_by_signal: bool,
}
#[workflow]
fn day_night(init: DayNightInit) -> Result<DayNightResult> {
let mut current_phase: u8 = match init.starting_phase {
TimeOfDay::Dawn => 0,
TimeOfDay::Day => 1,
TimeOfDay::Dusk => 2,
TimeOfDay::Night => 3,
};
let mut cycles_completed: u32 = 0;
let mut stopped: bool = false;
loop {
select! {
timer!(DayNightTimer::PhaseEnd, init.phase_duration_secs.secs()) => {
current_phase = (current_phase + 1) % 4;
if current_phase == 0 {
cycles_completed += 1;
}
continue
},
signal!(DayNightSignal::SkipPhase) => {
current_phase = (current_phase + 1) % 4;
if current_phase == 0 {
cycles_completed += 1;
}
continue
},
signal!(DayNightSignal::Stop) => {
stopped = true;
break
},
}.await;
}
Ok(DayNightResult {
cycles_completed,
final_phase: match current_phase {
0 => TimeOfDay::Dawn,
1 => TimeOfDay::Day,
2 => TimeOfDay::Dusk,
_ => TimeOfDay::Night,
},
stopped_by_signal: stopped,
})
}| Signal | Payload | Effect |
|---|---|---|
skip_phase |
— | Jump to next phase immediately |
stop |
— | End the cycle |
Key pattern: Infinite loop with explicit stop signal. Use correlation_id to create a singleton ("global_day_night") so you can find it later. Good for world-state workflows that run for the lifetime of the server.
Use case: Timed auction with bidding and optional instant buyout.
┌─────────────────────────────────────────────────────────────┐
│ AUCTION │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────┐ │
│ │ START │ Set starting price │
│ └───┬───┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ AWAITING_BIDS │◀─────────────────────┐ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ┌──────┼──────────────┬───────┐ │ │
│ │ │ │ │ │ │
│ timeout bid buyout cancel │ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │ │
│ end update bid ┌────────┐ end │ │
│ │ (if higher) │ SOLD │ (no bids) │ │
│ │ │ └────────┘ │ │
│ │ └───────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ COMPLETE │ (sold if had bids) │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};
#[derive(Timer)]
pub enum AuctionTimer { End }
#[derive(Signal)]
pub enum AuctionSignal {
Bid(BidData),
Buyout(u64), // bidder_id
Cancel,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct BidData {
pub bidder_id: u64,
pub amount: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuctionInit {
pub item_id: u64,
pub seller_id: u64,
pub starting_price: u64,
pub buyout_price: Option<u64>,
pub duration_secs: u64,
}
#[workflow]
fn auction(init: AuctionInit) -> Result<AuctionResult> {
let mut current_bid: u64 = init.starting_price;
let mut high_bidder: u64 = 0;
let mut sold: bool = false;
loop {
select! {
timer!(AuctionTimer::End, init.duration_secs.secs()) => {
if high_bidder > 0 { sold = true; }
break
},
signal!(AuctionSignal::Bid(bid)) => {
if bid.amount > current_bid {
current_bid = bid.amount;
high_bidder = bid.bidder_id;
}
continue
},
signal!(AuctionSignal::Buyout(bidder_id)) => {
if let Some(price) = init.buyout_price {
current_bid = price;
high_bidder = bidder_id;
sold = true;
break
}
continue
},
signal!(AuctionSignal::Cancel) => {
if high_bidder == 0 { break }
continue
},
}.await;
}
Ok(AuctionResult { sold, final_price: current_bid, winner_id: high_bidder })
}| Signal | Payload | Effect |
|---|---|---|
bid |
BidData | Update high bid if higher |
buyout |
bidder_id | End auction immediately at buyout price |
cancel |
— | Cancel if no bids yet |
Use case: Two-player item exchange requiring both parties to accept.
┌─────────────────────────────────────────────────────────────┐
│ TRADE │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────┐ │
│ │ START │ Both players not accepted │
│ └───┬───┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ AWAITING_ACCEPT │◀─────────────────────┐ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ┌──────┼──────────────┬───────┐ │ │
│ │ │ │ │ │ │
│ timeout accept(p1/p2) cancel modify │ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │ │
│ expire track who ┌────────┐ reset │ │
│ │ accepted │COMPLETE│ accepts │ │
│ │ │ │(cancel)│ │ │ │
│ │ │ └────────┘ │ │ │
│ │ │ │ │ │
│ │ both? ──No──────────────────┴─────┘ │
│ │ │ │
│ │ Yes │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ │
│ │ COMPLETE │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};
#[derive(Timer)]
pub enum TradeTimer { Expire }
#[derive(Signal)]
pub enum TradeSignal {
Accept(u64), // player_id
Cancel(u64), // player_id
ModifyOffer(u64), // player_id - resets acceptances
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TradeInit {
pub player1_id: u64,
pub player2_id: u64,
pub player1_offers: Vec<u64>,
pub player2_offers: Vec<u64>,
pub timeout_secs: u64,
}
#[workflow]
fn trade(init: TradeInit) -> Result<TradeResult> {
let mut player1_accepted: bool = false;
let mut player2_accepted: bool = false;
loop {
select! {
timer!(TradeTimer::Expire, init.timeout_secs.secs()) => break,
signal!(TradeSignal::Accept(player_id)) => {
if player_id == init.player1_id { player1_accepted = true; }
else if player_id == init.player2_id { player2_accepted = true; }
if player1_accepted && player2_accepted { break }
continue
},
signal!(TradeSignal::Cancel(_player_id)) => break,
signal!(TradeSignal::ModifyOffer(_player_id)) => {
player1_accepted = false;
player2_accepted = false;
continue
},
}.await;
}
Ok(TradeResult { completed: player1_accepted && player2_accepted })
}| Signal | Payload | Effect |
|---|---|---|
accept |
player_id | Mark player as accepted |
cancel |
player_id | End trade immediately |
modify_offer |
player_id | Reset both acceptances |
Key pattern: Multi-party coordination. Both parties must accept before completion. Modifying the offer resets both acceptances.
Use case: Multi-objective quest with progress tracking.
┌─────────────────────────────────────────────────────────────┐
│ QUEST │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────┐ │
│ │ START │ 0 objectives complete │
│ └───┬───┘ │
│ │ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ IN_PROGRESS │◀─────────────────────┐ │
│ └────────┬────────┘ │ │
│ │ │ │
│ ┌──────┼──────────────┬───────┐ │ │
│ │ │ │ │ │ │
│ timeout objective abandon fail │ │
│ │ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │ │
│ expire done++ ┌────────────────┐ │ │
│ │ │ │ COMPLETE │ │ │
│ │ │ │(abandon/fail) │ │ │
│ │ │ └────────────────┘ │ │
│ │ │ │ │
│ │ all done? ──No────────────────────┘ │
│ │ │ │
│ │ Yes │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────┐ │
│ │ COMPLETE │ │
│ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
use workflow_core::prelude::*;
use workflow_macros::{workflow, Timer, Signal};
#[derive(Timer)]
pub enum QuestTimer { Expire }
#[derive(Signal)]
pub enum QuestSignal {
ObjectiveComplete(u32), // objective_index
Abandon,
Fail,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QuestInit {
pub quest_id: String,
pub player_id: u64,
pub objective_count: u32,
pub time_limit_secs: Option<u64>,
}
#[workflow]
fn quest(init: QuestInit) -> Result<QuestResult> {
let mut objectives_done: u32 = 0;
let mut abandoned: bool = false;
let mut failed: bool = false;
let timeout = init.time_limit_secs.unwrap_or(86400);
loop {
select! {
timer!(QuestTimer::Expire, timeout.secs()) => {
if init.time_limit_secs.is_some() { failed = true; }
break
},
signal!(QuestSignal::ObjectiveComplete(index)) => {
if index < init.objective_count { objectives_done += 1; }
if objectives_done >= init.objective_count { break }
continue
},
signal!(QuestSignal::Abandon) => { abandoned = true; break },
signal!(QuestSignal::Fail) => { failed = true; break },
}.await;
}
Ok(QuestResult {
completed: objectives_done >= init.objective_count && !abandoned && !failed,
objectives_done,
abandoned,
failed,
})
}| Signal | Payload | Effect |
|---|---|---|
objective_complete |
index | Increment progress |
abandon |
— | Player gave up |
fail |
— | Quest failed (e.g., NPC died) |
Key pattern: Progress tracking with multiple exit conditions. Use entity_id to attach to player for easy querying.
| Pattern | Example | Technique |
|---|---|---|
| Looping | Patrol waypoints | loop { select! { ... }.await } |
| Interruptible | Buff dispel | signal!() arm with break |
| Uninterruptible | Respawn | No signal arms, only timer |
| Timeout | Combat actions | Timer arm with default action |
| Child spawn | Patrol → Combat | spawn!("workflow", init).await |
| Pause/Resume | Production halt | State flag + conditional select |
| Counter loop | Parent-child | for i in 0..n { spawn!().await } |
| Conditional | Feature flags | if cond { timer!().await } |
| Payload binding | Stack amount | signal!(Signal::Stack(n)) |
→ API Reference — Complete API documentation → Deployment — Production setup