Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Control Plane

The control plane gives SCHEMABOUND a structured way to track and execute multi-step agent workflows. Instead of issuing tool calls one at a time with no shared state, a client can submit a complete plan and drive execution step by step, receiving a feedback object after each step that carries tool output and any schema changes back to the LLM for the next invocation.

Why a Control Plane

A single tool call is stateless. The agent asks a question, SCHEMABOUND answers it, and the conversation moves on. Most production workflows are not that simple — they involve dependent queries, operations that discover schema at runtime, and decisions that build on earlier results.

Without a control plane:

  • the LLM must track intermediate state itself, which is unreliable across long conversations
  • schema changes that occur mid-workflow reach the LLM late or not at all
  • there is no stable record of what the agent intended versus what actually executed

The control plane solves this by making the plan a first-class object that persists through the full execution lifecycle.

Concepts

Plan

A plan is a named, versioned collection of steps with explicit dependency relationships. Steps declare their dependencies with depends_on; the control plane validates the dependency graph before any step executes and rejects cycles.

A submitted plan is assigned a stable plan_id that clients use for all subsequent operations.

Step

Each step in a plan corresponds to one tool call. A step carries:

  • a tool_name and tool_intent that govern policy evaluation
  • a query_template that may reference prior step output via the {{step.<id>.output}} syntax
  • schema_table_hints that tell the control plane which tables to snapshot for schema diffing
  • a depends_on list naming steps that must complete before this step can execute

LLM Context Update

When a step finishes, the control plane returns an LlmContextUpdate alongside the step result. This is the explicit feedback object the SDK passes to the next LLM API call.

It carries:

  • tool_output_json — the serialised result of the step, ready to include in the next message
  • schema_additions — a list of SchemaTableDelta entries (NEW, MODIFIED, or REMOVED) for any tables named in schema_table_hints that changed during step execution
  • augmentation_hints — human-readable strings derived from schema deltas, ready to append to the system prompt

The LLM always sees the current schema state before choosing its next action, which means tool definitions stay accurate even when schema evolves mid-workflow.

Template Substitution

Query templates support {{step.<id>.output}} placeholders. The control plane resolves these server-side before calling the query service — the LLM does not need to construct final SQL or query strings directly.

For example, a step template like:

SELECT * FROM orders WHERE customer_id = {{step.lookup_customer.output}}

becomes a fully resolved query once the lookup_customer step has completed and its output is available.

gRPC API

The control plane is exposed as a gRPC service defined in schemabound-proto.

service ControlPlaneService {
  rpc SubmitPlan    (SubmitPlanRequest)         returns (SubmitPlanResponse);
  rpc GetPlanStatus (GetPlanStatusRequest)      returns (GetPlanStatusResponse);
  rpc ExecuteStep   (ExecuteStepRequest)        returns (ExecuteStepResponse);
  rpc CancelPlan    (CancelPlanRequest)         returns (CancelPlanResponse);
  rpc StreamPlanEvents (StreamPlanEventsRequest) returns (stream PlanEvent);
}

Submit a Plan

message SubmitPlanRequest {
  string session_id   = 1;
  string name         = 2;
  string description  = 3;
  repeated PlanStepDef steps = 4;
}

message PlanStepDef {
  string step_id              = 1;
  string name                 = 2;
  string tool_name            = 3;
  string intent               = 4;
  string query_template       = 5;
  repeated string depends_on         = 6;
  repeated string schema_table_hints = 7;
}

A successful response returns a plan_id. All subsequent calls reference this identifier.

Execute a Step

message ExecuteStepRequest {
  string plan_id = 1;
  string step_id = 2;
}

message ExecuteStepResponse {
  bool              success           = 1;
  StepStatus        step_result       = 2;
  LlmContextUpdate  llm_context_update = 3;
  string            error_message     = 4;
}

The llm_context_update field is the value to pass back to the LLM before it chooses the next step.

Stream Plan Events

message StreamPlanEventsRequest {
  string plan_id = 1;
}

message PlanEvent {
  string plan_id    = 1;
  string event_type = 2;
  string payload_json = 3;
  string timestamp  = 4;
}

Event types include PlanCreated, PlanStepExecuted, PlanCompleted, and PlanFailed.

REST API

The control plane is also accessible over HTTP.

Create a Plan

POST /api/plans
Content-Type: application/json
{
  "session_id": "sess-abc123",
  "name": "Customer order lookup",
  "description": "Find customer then retrieve their orders",
  "steps": [
    {
      "id": "lookup_customer",
      "name": "Look up customer by email",
      "tool_name": "query_customers",
      "intent": "read_select",
      "query_template": "SELECT id FROM customers WHERE email = 'alice@example.com'",
      "depends_on": [],
      "schema_table_hints": ["customers"]
    },
    {
      "id": "get_orders",
      "name": "Get orders for customer",
      "tool_name": "query_orders",
      "intent": "read_select",
      "query_template": "SELECT * FROM orders WHERE customer_id = {{step.lookup_customer.output}}",
      "depends_on": ["lookup_customer"],
      "schema_table_hints": ["orders"]
    }
  ]
}

Response

{
  "data": {
    "plan_id": "plan-8f1c2b3d",
    "status": "pending"
  }
}

Get Plan Status

GET /api/plans/:plan_id

Response

{
  "data": {
    "plan_id": "plan-8f1c2b3d",
    "status": "running",
    "steps": [
      { "step_id": "lookup_customer", "status": "completed" },
      { "step_id": "get_orders",      "status": "pending"   }
    ]
  }
}

Execute a Step

POST /api/plans/:plan_id/steps/:step_id/execute

Response

{
  "data": {
    "step_result": {
      "step_id": "lookup_customer",
      "status": "completed",
      "row_count": 1,
      "executed_at": "2026-04-20T14:00:00Z"
    },
    "llm_context_update": {
      "plan_id": "plan-8f1c2b3d",
      "step_id": "lookup_customer",
      "tool_output_json": "{\"id\": 42}",
      "schema_additions": [],
      "augmentation_hints": []
    }
  }
}

Cancel a Plan

DELETE /api/plans/:plan_id

Runtime Context Headers

Steps execute under the same gRPC metadata model as ordinary queries. Two additional headers carry control-plane identity:

HeaderPurpose
x-schemabound-plan-idIdentifies the active plan for audit and event correlation
x-schemabound-step-indexPosition of the executing step within the plan

These are emitted into query events alongside the standard session, user, and organization fields.

OSS Trait Contract

The WorkflowOrchestrator trait in schemabound-public defines the full public interface. Any implementation — including custom ones — must satisfy:

#![allow(unused)]
fn main() {
#[async_trait]
pub trait WorkflowOrchestrator: Send + Sync {
    async fn create_plan(
        &self,
        session_id: &str,
        definition: PlanDefinition,
    ) -> Result<PlanRecord, String>;

    async fn get_plan(
        &self,
        plan_id: &str,
    ) -> Result<Option<PlanRecord>, String>;

    async fn execute_step(
        &self,
        plan_id: &str,
        step_id: &str,
        ctx: &QueryRuntimeContext,
    ) -> Result<(StepResult, LlmContextUpdate), String>;

    async fn cancel_plan(
        &self,
        plan_id: &str,
    ) -> Result<(), String>;
}
}

NoOpWorkflowOrchestrator is the default in OSS builds. It satisfies the trait boundary and returns an explicit error on any write operation, making the absence of a backing store visible rather than silent.

Event Integration

Control-plane events flow through the same global EventBus used by the rest of the runtime. Four new event variants are available to handlers:

EventWhen emitted
PlanCreatedPlan accepted and persisted
PlanStepExecutedA step finished (success or failure)
PlanCompletedAll steps completed successfully
PlanFailedA step failed or the plan was cancelled

Existing AuditLogHandler, QueryMetricsHandler, and SessionActivityHandler receive these events the same way they receive query events — no changes to handler registration are needed.