-`workflow_executions` and `workflow_events` tables exist in DB
-`WorkflowExecution` / `WorkflowEvent` models exist in `core/shared/models/workflow_models.rs`
-`ORCHESTRATE WORKFLOW` keyword exists in `basic/keywords/orchestration.rs` (stub)
-`STEP` keyword registered but not durable
- Compiler (`basic/mod.rs`) produces Rhai AST and runs it in one shot via `engine.eval_ast_with_scope`
-`HEAR` currently blocks a thread (new) — works but not crash-safe
---
## Goal
BASIC scripts run as **durable step sequences**. Each keyword is a step. On crash/restart, execution resumes from the last completed step. No re-run. No Rhai for control flow.
```basic
' ticket.bas
TALK "Describe the issue" ← Step 1
HEAR description ← Step 2 (suspends, waits)
SET ticket = CREATE(description) ← Step 3
TALK "Ticket #{ticket} created" ← Step 4
```
---
## Two Execution Modes
The compiler serves both modes via a pragma at the top of the `.bas` file:
```basic
' Default: Rhai mode (current behavior, fast, no durability)
TALK "Hello"
' Workflow mode (durable, crash-safe)
#workflow
TALK "Hello"
HEAR name
```
`ScriptService::compile()` detects `#workflow` and returns either:
-`ExecutionPlan::Rhai(AST)` — current path, unchanged
Expressions inside steps (`condition`, `expression`, `template`) are still evaluated by Rhai — but only as **pure expression evaluator**, no custom syntax, no side effects. This keeps Rhai as a math/string engine only.
SET x = score + 1 → Step::Set { var, expr: "score + 1" }
IF score > 10 THEN → Step::If { cond: "score > 10", then_steps, else_steps }
SEND MAIL to, s, b → Step::SendMail { to, subject, body }
USE TOOL path → Step::UseTool { path, args }
```
Expressions (`score + 1`, `score > 10`) are stored as **raw strings** in the Step struct.
At runtime, Rhai evaluates them as pure expressions — no custom syntax, no side effects:
```rust
let mut engine = Engine::new(); // no register_custom_syntax calls
let mut scope = Scope::new();
for (k, v) in &variables { scope.push_dynamic(k, v.clone()); }
let result = engine.eval_expression_with_scope::<Dynamic>(&mut scope, expr)?;
```
Rhai remains a dependency but is used only as a math/string expression evaluator (~5 lines of code at runtime). All custom keyword machinery is bypassed entirely.
**Why custom over Restate:** Restate requires its own server as a proxy between HTTP requests and handlers — adds a network hop and an extra process. The custom plan uses PostgreSQL already running in the stack, zero extra infrastructure.
**Escape hatch:** The `Step` enum in this plan maps 1:1 to Restate workflow steps. If the custom engine proves too complex to maintain, migration to Restate is mechanical — swap `WorkflowEngine::execute_step` internals, keep the compiler and Step enum unchanged.