Plans
The plan module enables Loquent to autonomously execute multi-step communication workflows. An AI executor follows plan template descriptions — sending emails, SMS messages, gathering contact info, and writing notes — with human approval required for every outbound communication.
How It Works
Section titled “How It Works”Trigger (call end, inbound SMS, or manual) → LLM generates plan title + description from context → Plan created in PendingReview or StandBy (if auto-approve)
User reviews plan → Approve or Reject
Approved plan → StandBy (next_execution_at = now)
Every 30 seconds: → execute_plans_job picks up StandBy plans past next_execution_at → execute_plan(plan_id) LLM agentic loop with 13 tools → Outbound tools (email/SMS): logged as Pending, plan pauses → Infrastructure tools: executed immediately
User approves/rejects pending tool calls in timeline UI → Approved tool calls execute via background job → Plan resumes automatically after resolutionCore Concepts
Section titled “Core Concepts”Plan Template
Section titled “Plan Template”A reusable definition that controls when and how plans are created.
pub struct PlanTemplate { pub id: Uuid, pub name: String, // e.g. "Post-Call Follow-up" pub description: String, // Natural-language workflow instructions pub trigger: String, // "post_call" | "new_sms" | "manual" pub auto_approve: bool, // Skip PendingReview when true pub autopilot: bool, // Outbound actions execute without per-action approval pub re_enrollment_policy: String, // "once" | "multiple" (default: "multiple") pub start_condition: Option<String>, // LLM-evaluated trigger filter pub sender_phone_number_id: Option<Uuid>, // Override SMS sender phone pub is_active: bool, pub actions: Vec<Action>, // Authorized capabilities}The description field is the executor’s primary instruction set. It tells the AI what steps to follow, what to communicate, and in what order.
Start Condition
Section titled “Start Condition”The start_condition field is an optional free-text condition that the LLM evaluates against trigger events to decide whether a template is relevant. This gives you fine-grained control over when plans fire, beyond just the trigger type.
Example: A post_call template with start condition "The caller expressed interest in scheduling a demo" only creates a plan when the call transcription matches that intent.
The behavior varies by trigger type:
| Trigger | Role | Effect |
|---|---|---|
post_call | Hard filter | LLM returns is_relevant: bool. If false, no plan is created. |
new_sms | Hard filter | LLM returns is_relevant: bool. If false, no plan is created. |
manual | Intent context | Always creates a plan. The condition provides context to improve the generated description. |
For event-triggered templates, the LLM evaluates semantic relevance — it focuses on meaning, not exact wording. If the event has nothing to do with the start condition’s intent, the template is skipped.
When start_condition is None or empty, the LLM prompt omits the condition section entirely and templates behave as before.
Action
Section titled “Action”A system-defined capability that serves as a permission boundary for the executor.
pub struct Action { pub id: Uuid, pub name: String, // e.g. "Send Email", "Send SMS" pub description: String, // Prompt fragment injected into system prompt pub required_tools: Vec<PlanTool>, // Capability tools this action needs}Each action declares which capability tools it requires via required_tools. The executor collects the union of required tools across all assigned actions and only includes those capability tools in the LLM request. Infrastructure and lifecycle tools are always included.
#[derive(Serialize, Deserialize, Hash, Eq, PartialEq)]#[serde(rename_all = "snake_case")]pub enum PlanTool { // Capability tools — conditionally included SendEmail, SendSms, UpdateContact, // Infrastructure and lifecycle tools — always included ListPlanContacts, GetContactDetails, GetContactNotes, GetConversationHistory, WriteInteractionNote, UpdateSystemNote, AskUser, NotifyUser, CompletePlan, FailPlan, ScheduleNextExecution,}The required_tools column is a JSONB array on the action table, stored as snake_case strings (e.g. ["send_email"]). If an action doesn’t need any capability tools, the array is empty and only infrastructure/lifecycle tools are available.
A specific workflow instance created from a template, tied to one or more contacts.
pub struct Plan { pub id: Uuid, pub title: String, // LLM-generated from call context pub description: String, // LLM-generated workflow steps pub state: PlanState, pub plan_template_id: Option<Uuid>, // Source template (nullable, ON DELETE SET NULL) pub sender_phone_number_id: Option<Uuid>, // Copied from template at creation pub actions: Vec<Action>, // Inherited from template pub contacts: Vec<PlanContactSummary>,}Plan State Machine
Section titled “Plan State Machine”Plan state is stored as a tagged JSONB enum. Each variant carries only state-specific data.
| State | Meaning |
|---|---|
PendingReview | Created, awaiting human approval |
StandBy { next_execution_at } | Approved, queued for executor pickup |
Executing | Executor is actively running the LLM loop |
AwaitingInput | Paused — pending tool calls need human action |
Paused | User-suspended execution, resumable |
Completed { completed_at } | All steps finished successfully |
Failed { error_message } | Unrecoverable failure |
Stopped | Permanently cancelled by user (terminal) |
Tool Call State Machine
Section titled “Tool Call State Machine”Each outbound tool call (email, SMS) follows its own lifecycle within the plan log:
| State | Meaning |
|---|---|
Pending | Logged by executor, awaiting human approval |
Approved | User approved, waiting for background job execution |
Done { value } | Successfully executed |
Rejected | User rejected this action |
Expired | Past expires_at window, not executed |
Failed { error } | Execution attempted but failed |
Plan Creation
Section titled “Plan Creation”Plans are created through three triggers using a two-step AI flow. For event-triggered plans (call/SMS), a cheap assessment model first checks whether the template is relevant, then a capable model generates the plan. See AI model split for details.
Post-Call
Section titled “Post-Call”When a call ends, create_plans_from_call matches active templates with the post_call trigger. For each template:
- Assessment — If the template has a start condition, the assessment model evaluates semantic relevance against the call transcription. If
is_relevantisfalse, the template is skipped. - Instantiation — The instantiation model receives the call transcription and generates a contextual title and description.
Inbound SMS
Section titled “Inbound SMS”When an inbound SMS arrives via the Twilio webhook, create_plans_from_sms queries all active templates with the new_sms trigger for the organization. Each template follows the same two-step flow:
- Assessment — If a start condition is set, the assessment model checks relevance against the SMS body.
- Instantiation — The instantiation model generates the plan:
struct GeneratedPlan { title: String, // Max 100 chars description: String, // Context-focused workflow steps}If assessment returns is_relevant: false, no plan is created for that template — this prevents irrelevant messages from generating noise. When relevant, the plan is created with its actions and contact associations, and a plan.created event is published.
The SMS trigger runs as a fire-and-forget async spawn inside handle_inbound_message — failures are logged but never block SMS ingestion. This runs alongside the existing inbound wake logic, so a single SMS can both wake existing plans and create new ones.
Manual (from Contact Sidebar)
Section titled “Manual (from Contact Sidebar)”Users create plans on demand from the contact details sidebar. The Plans section shows existing plans with status badges and a template picker.
Flow:
- User selects a template from the dropdown and clicks Start
POST /api/planswith{ template_id, contact_id }create_plan_from_templateloads the contact’s details and notes- The instantiation model generates a title (referencing the contact by name) and description. If the template has a start condition, it’s included as intent context to make the description more specific. Assessment is skipped — user intent is implicit.
- If
template.auto_approveis true → plan starts inStandBy(executes immediately) - Otherwise → plan starts in
PendingReview - Plan appears in the sidebar list with its state badge
// Request payloadpub struct CreatePlanInput { pub template_id: Uuid, pub contact_id: Uuid,}The LLM system prompt instructs the model to focus on relationship context and intent — it does not include specific contact details (phone, email) because the executor looks those up at runtime via tools.
AI Model Split
Section titled “AI Model Split”Plan creation uses two separate AI models optimized for their respective tasks:
| Step | AI Area | Default Model | Purpose |
|---|---|---|---|
| Assessment | AssessPlanTemplateStartCondition | Gemini 3.1 Flash Lite | Binary relevance check — is this template relevant to the event? |
| Instantiation | InstantiatePlan | Claude Opus 4.6 | Generate plan title and description from context |
Why split? Assessment is a simple yes/no classification. Using a lightweight model (Gemini Flash Lite at $0.075/M tokens) instead of Opus ($15/M tokens) cuts assessment costs by ~200×.
Assessment step
Section titled “Assessment step”Runs only for event-triggered plans (post_call, new_sms) when the template has a non-empty start_condition. Returns a structured response:
struct Assessment { is_relevant: bool, // Should this template fire? reasoning: String, // Brief explanation}If is_relevant is false, the template is skipped and no instantiation call is made. Templates without a start condition skip assessment entirely.
Instantiation step
Section titled “Instantiation step”Runs for all triggers. Returns the plan content:
struct GeneratedPlan { title: String, // Short, specific (max 100 chars) description: String, // Detailed steps for the executor}The instantiation model respects template.model_override — if set, it uses that model instead of the system default resolved from AiArea::InstantiatePlan.
Usage tracking
Section titled “Usage tracking”Each step logs separately to ai_usage_log:
- Assessment →
AiUsageFeature::AssessPlanTemplate - Instantiation →
AiUsageFeature::InstantiatePlan
This enables granular cost analysis per step in the Cost & Usage Dashboard.
Configuration
Section titled “Configuration”Both models are configurable in Admin → AI Models. See AI Model Configuration for details.
Autopilot Mode
Section titled “Autopilot Mode”The autopilot boolean on plans controls whether outbound actions (emails, SMS) execute immediately or pause for human approval. Templates set the default; users override per-plan at runtime.
How It Works
Section titled “How It Works”When autopilot is enabled, the executor sends emails and SMS immediately. The LLM receives a prompt indicating autopilot mode and continues execution without stopping. Timeline entries log directly as Done or Failed.
When autopilot is disabled (default), outbound actions log as Pending and the plan transitions to AwaitingInput. A human reviews each action in the timeline UI before it executes.
The ask_user tool always pauses regardless of autopilot setting — user input is required.
Autopilot vs Auto-Approve
Section titled “Autopilot vs Auto-Approve”These are orthogonal settings controlling different approval stages:
| Setting | Scope | Controls |
|---|---|---|
auto_approve | Plan creation | Whether new plans skip PendingReview and start in StandBy |
autopilot | Tool execution | Whether outbound actions execute immediately or wait for per-action approval |
The four combinations:
| auto_approve | autopilot | Behavior |
|---|---|---|
| ✗ | ✗ | Plan requires approval to start. Each outbound action requires approval. |
| ✓ | ✗ | Plan starts automatically. Each outbound action requires approval. |
| ✗ | ✓ | Plan requires approval to start. Once approved, actions execute immediately. |
| ✓ | ✓ | Fully autonomous. Only ask_user pauses execution. |
Toggle API
Section titled “Toggle API”Users toggle autopilot at runtime from the plan details view:
PUT /api/plans/{id}/autopilot{ "autopilot": true }Requires Plan:Instance:Approve permission. The UI uses optimistic updates — the toggle reflects immediately and reverts on error.
Template Configuration
Section titled “Template Configuration”Set the default autopilot value when creating or editing a template. The plan inherits this value at creation time. The PlanTemplateData request body includes:
pub struct PlanTemplateData { pub name: String, pub description: String, pub trigger: String, pub auto_approve: bool, pub autopilot: bool, // Default for plans created from this template pub re_enrollment_policy: String, // "once" | "multiple" (default: "multiple") pub start_condition: Option<String>, // LLM-evaluated trigger filter pub sender_phone_number_id: Option<Uuid>, // Override SMS sender phone pub is_active: bool, pub action_ids: Vec<Uuid>,}Active plans with autopilot enabled show a blue Autopilot badge in the plan list and template cards.
Re-enrollment Policy
Section titled “Re-enrollment Policy”The re_enrollment_policy field on plan templates controls whether the same contact can be enrolled in the same template more than once.
| Policy | Behavior |
|---|---|
multiple | Default. A contact can be enrolled any number of times. |
once | A contact can only be enrolled once per template. Duplicate attempts are silently skipped. |
How It Works
Section titled “How It Works”When a plan is created, the system stores the source plan_template_id on the plan record. For templates with re_enrollment_policy: "once", the creation service queries existing plans to check whether any plan already exists for that contact-template pair. If a match is found, the plan is not created.
This check runs in all three creation paths:
- Manual — the API returns an error if the contact already has a plan from this template. The UI also filters out
oncetemplates that the contact has already been enrolled in. - Post-call — the service silently skips the template. Other matching templates still create plans.
- Inbound SMS — same as post-call: silently skipped, logged at INFO level.
Template Configuration
Section titled “Template Configuration”Set the re-enrollment policy when creating or editing a template via PlanTemplateData (see Template Configuration above for the full struct).
The UI presents this as a radio group labeled Re-enrollment in the template form. Templates with once policy show a badge in the template list.
Executor Architecture
Section titled “Executor Architecture”The executor uses Claude Sonnet for tool-calling execution and Claude Opus for plan creation from calls. Model constants are defined in src/mods/ai/types/ai_models_type.rs.
The executor runs as a multi-phase pipeline inside execute_plan:
- Validate — confirm plan is in
StandBy, transition toExecuting - Gate — if any tool calls are still
PendingorApproved, transition toAwaitingInputand exit - LLM loop — build system prompt from assigned actions, assemble timeline context, run the agentic tool-calling loop with fallback retry
- Error check — if the LLM call failed, fail the plan immediately with the actual error message
- Safety check — if the LLM succeeded but exited without calling a terminal tool (
complete_plan,fail_plan, orschedule_next_execution), auto-fail the plan
Fallback Model Retry
Section titled “Fallback Model Retry”The executor tries two models in sequence. If the primary model fails (timeout, rate limit, API error), it retries with a fallback model before giving up.
pub const EXECUTE_PLAN: &str = "anthropic/claude-sonnet-4.6";pub const EXECUTE_PLAN_FALLBACK: &str = "google/gemini-3.1-pro-preview";The retry loop iterates over both models, building fresh tools for each attempt. On success, the executor records which model actually ran — this feeds into AI usage tracking so billing reports reflect the real model used, not always the primary.
LLM Error Handling
Section titled “LLM Error Handling”When both models fail, the executor fails the plan immediately with the actual error message. This happens before the safety check — the error check at phase 4 takes priority.
LLM call failed → Set plan state to Failed { error_message: "LLM execution error: <actual error>" } → Log RuntimeAutoFail with the real error reason → Send notification: "Plan execution failed due to an LLM error." → Early return (skip safety check)The error_message in PlanState::Failed contains the specific API error (e.g., “Rate limit exceeded”, “Request timeout after 60s”), visible in the plan timeline UI. A notification alerts all org members immediately.
Executor Tools
Section titled “Executor Tools”The executor builds its tool set dynamically based on the plan’s assigned actions. Infrastructure and lifecycle tools are always included. Capability tools (send_email, send_sms) are only included when at least one assigned action lists them in required_tools. This keeps the LLM context focused — a plan that only sends SMS never sees the send_email tool schema.
Capability (conditionally included, approval-gated):
| Tool | Description |
|---|---|
send_email | Queue an email for approval. Supports scheduled_at and expires_at. Sends a Plan notification when approval is needed. |
send_sms | Queue an SMS for approval. Supports scheduled_at and expires_at. Sends a Plan notification when approval is needed. |
update_contact | Update contact profile fields (name, company, pipeline stage, etc.). Executes immediately — no approval required. |
Infrastructure (immediate execution):
| Tool | Description |
|---|---|
list_plan_contacts | List contacts associated with this plan |
get_contact_details | Get a contact’s name, phones, and emails |
get_contact_notes | Read a contact’s notes |
get_conversation_history | Read message history with optional channel filter and limit (1–50, default 20) |
write_interaction_note | Create an interaction note on a contact |
update_system_note | Record new facts about a contact — routes through the AI memory pipeline for automatic merging and deduplication |
Lifecycle (flow control and user communication):
| Tool | Description |
|---|---|
ask_user | Ask the user a question with options or free text (always pauses, even in autopilot) |
notify_user | Send an in-app notification to all org members. Use for significant events: messages sent, replies received, warm leads detected. |
complete_plan | Mark the plan as completed (terminal) |
fail_plan | Mark the plan as failed with a reason (terminal) |
schedule_next_execution | Pause and re-execute after a delay in minutes |
When an outbound tool is called, the executor logs it as Pending and the plan transitions to AwaitingInput. The user approves or rejects each tool call from the timeline UI.
Plan Notifications
Section titled “Plan Notifications”The plan module integrates with the notification system at both the AI and system levels.
AI-driven notifications — The notify_user tool lets the executor send contextual notifications during execution. The system prompt instructs the LLM to notify users about significant events like messages sent, replies received, or noteworthy situations. Input: title (short headline) and body (optional detail text). All notifications use the Plan category and link back to the plan entity.
System-level notifications — Hardcoded emit points fire notifications for events outside the executor’s control:
| Event | Target | Message |
|---|---|---|
| Plan created needing review | All members | Plan needs review |
| Outbound email queued (non-autopilot) | All members | Plan action needs approval (email) |
| Outbound SMS queued (non-autopilot) | All members | Plan action needs approval (SMS) |
| Inbound SMS wakes a plan | All members | Plan woke from inbound reply |
| Executor auto-fail (no terminal tool) | All members | Plan execution failed |
Scheduling
Section titled “Scheduling”Outbound tools accept optional timing fields:
scheduled_at— ISO 8601 UTC timestamp. The background job waits until this time to execute.expires_at— ISO 8601 UTC timestamp. If the job hasn’t executed by this time, it transitions toExpired.
If omitted, the tool call executes as soon as a human approves it.
Timezone-Aware Display
Section titled “Timezone-Aware Display”All plan and plan template timestamps display in the organization’s configured timezone with a visible UTC offset (e.g., 2026-03-20 10:30:00 -04:00). The four affected API endpoints — get_plan, get_plans, get_plan_template, get_plan_templates — resolve the org timezone via resolve_org_timezone() and format created_at, updated_at, and state timestamps through format_in_tz().
PlanState variants that carry timestamps (StandBy.next_execution_at, Completed.completed_at) are converted via with_display_tz():
impl PlanState { pub fn with_display_tz(self, tz: &chrono_tz::Tz) -> Self { match self { PlanState::StandBy { next_execution_at } => { let converted = NaiveDateTime::parse_from_str(&next_execution_at, "%Y-%m-%d %H:%M:%S%.f") .map(|dt| format_in_tz(&dt, tz)) .unwrap_or(next_execution_at); PlanState::StandBy { next_execution_at: converted } } // Completed.completed_at is also converted _ => self, } }}The executor’s LLM system prompt also includes the org’s timezone and current local time, so the AI can reason about scheduling in the user’s local context rather than raw UTC.
Inbound Wake
Section titled “Inbound Wake”When a contact with active StandBy plans sends an inbound SMS, the system automatically wakes those plans by setting next_execution_at to now. This lets plans respond to incoming messages within ~30 seconds (the next executor poll cycle).
The wake service (wake_plans_for_contact) queries the plan_contact junction table to find all plans linked to the sender, filters for StandBy state, and updates their execution time. A notification is sent to all org members when plans wake.
This enables conversational workflows — a plan sends an SMS, schedules its next execution for the future, and resumes immediately when the contact replies.
SMS Phone Routing
Section titled “SMS Phone Routing”When the executor sends an SMS, it selects the “from” phone number using a three-level priority:
- Explicit sender — If the plan has a
sender_phone_number_id, use that phone number. This is set on the plan template and copied to the plan at creation time. - Contact history — Reuse the org phone number from the contact’s most recent SMS conversation. This maintains thread continuity on the contact’s device.
- Org default — Fall back to the first phone number configured for the organization.
Sender Phone Override
Section titled “Sender Phone Override”The sender_phone_number_id field on plan templates lets you pin SMS steps to a specific Twilio number. Set it in the template form by selecting a phone from the Sender Phone dropdown. Choose “Default (auto-detect)” to clear the override and fall back to the contact history / org default logic.
The field is copied from template to plan at creation time — changing a template’s sender phone does not affect existing plans. If the referenced phone number is deleted, the foreign key cascades to NULL and the executor falls back to the standard routing logic.
Background Jobs
Section titled “Background Jobs”Two jobs run every 30 seconds:
| Job | Purpose |
|---|---|
ExecutePlansJob | Picks up plans in StandBy past their next_execution_at and runs execute_plan |
ExecuteScheduledToolCallsJob | Processes Approved tool calls — executes at scheduled_at or expires them past expires_at |
Real-Time Events
Section titled “Real-Time Events”Plan views update instantly via WebSocket events broadcast through the EventHub. Three event types cover the full plan lifecycle:
| Event | Constant | Scope | Payload |
|---|---|---|---|
plan.log.new | EVT_PLAN_LOG_NEW | Organization | PlanLogEvent |
plan.state.changed | EVT_PLAN_STATE_CHANGED | Organization | PlanStateChanged |
plan.created | EVT_PLAN_CREATED | Organization | Plan |
All events are published at the organization level — every connected user in the org receives them.
Event Payloads
Section titled “Event Payloads”// Wraps a log entry with the plan_id for client-side filteringpub struct PlanLogEvent { pub plan_id: Uuid, #[serde(flatten)] pub log: PlanLogDisplay, // The full log entry}
// Emitted when a plan transitions between statespub struct PlanStateChanged { pub plan_id: Uuid, pub state: PlanState, // The new state (StandBy, Executing, etc.)}The plan.created event sends the full Plan struct as its payload.
Server-Side Publishing
Section titled “Server-Side Publishing”Three helpers in publish_plan_event_service.rs handle broadcasting:
publish_plan_log(org_id, plan_id, &log_display); // New or updated log entrypublish_plan_state(org_id, plan_id, &state); // State transitionpublish_plan_created(org_id, &plan); // New plan createdThese are called from the executor service (state transitions), build tools (terminal tools), plan APIs (approve/reject actions), and plan creation flows.
Client-Side Hooks
Section titled “Client-Side Hooks”Two Dioxus hooks subscribe to these events:
use_realtime_plan(plan_id) — For the detail view. Returns (live_logs, live_state) signals. Filters events by plan_id, deduplicates log entries by ID (updates existing entries in-place), and tracks the latest state.
use_realtime_plans() — For the list view. Returns (state_changes, new_plans) signals. Tracks state changes across all plans and appends newly created plans. The list view merges these with server-fetched data.
Both hooks read from RealtimeContext and process events in a use_effect loop.
AI Template Generation
Section titled “AI Template Generation”Plan templates support AI-powered description generation. Users enter a natural-language goal (e.g. “After a sales call, send a follow-up email summarizing the discussion”), and the system returns:
- A structured workflow description for the executor
- Suggested actions to assign (pre-checked in the form)
The generation endpoint uses structured output to return both the description and an array of action IDs. An edit/refine toolbar (Improve, Make Concise, Fix, etc.) is available via the shared AI builder infrastructure.
API Endpoints
Section titled “API Endpoints”All endpoints require an authenticated session.
| Route | Method | Description |
|---|---|---|
/api/plans | GET | List all org plans (newest first) |
/api/plans | POST | Create a plan from a template for a contact (CreatePlanInput) |
/api/plans/{id} | GET | Get plan details with timeline log |
/api/plans/{id}/autopilot | PUT | Toggle autopilot mode ({ "autopilot": bool }) |
/api/plans/{id}/approve | PUT | Approve a pending plan → StandBy |
/api/plans/{id}/reject | PUT | Reject a pending plan → Failed |
/api/plans/{id}/logs/{log_id}/approve | PUT | Approve a pending tool call |
/api/plans/{id}/logs/{log_id}/reject | PUT | Reject a pending tool call |
/api/plans/{id}/logs/{log_id}/answer | PUT | Answer an ask_user question |
/api/contacts/{contact_id}/plans | GET | List plans for a specific contact |
Plan Templates
Section titled “Plan Templates”| Route | Method | Description |
|---|---|---|
/api/plan-templates | GET | List all org templates with actions |
/api/plan-templates/{id} | GET | Get a single template |
/api/plan-templates | POST | Create a template with name, description, trigger, and actions |
/api/plan-templates/{id} | PUT | Update a template |
/api/plan-templates/{id} | DELETE | Delete a template |
Actions
Section titled “Actions”| Route | Method | Description |
|---|---|---|
/api/actions | GET | List all available system actions |
Module Structure
Section titled “Module Structure”src/mods/plan/├── api/ # CRUD for plans, templates, approval/rejection, answer├── components/ # UI: plan list, cards, timeline, template management├── jobs/ # execute_plans_job, execute_scheduled_tool_calls_job├── services/ # execute_plan, create_plans_from_call, execute_send_email, execute_send_sms├── tools/ # build_tools (12 executor tools), log_tool_call├── types/ # PlanState, PlanLogEntry, Plan, PlanTemplate, Action└── views/ # PlanListView, PlanDetailsView, PlanTemplateListView, etc.Database Tables
Section titled “Database Tables”| Table | Purpose |
|---|---|
action | System-defined capabilities (seeded) |
plan_template | Reusable workflow definitions (includes re_enrollment_policy, start_condition) |
plan_template_action | Join: template ↔ actions |
plan | Workflow instances with JSONB state and optional plan_template_id FK |
plan_action | Join: plan ↔ actions |
plan_contact | Join: plan ↔ contacts |
plan_log | Timeline entries (JSONB entry column) |
Related Modules
Section titled “Related Modules”- Todos — simpler single-action task extraction from calls
- Call — plans are triggered by completed calls
- AI Builder — shared infrastructure for template description generation