Crux
API Reference@crux/core

Plans

Structured primitives for agent work planning with version tracking and agent integration.

import {
  plan, getPlan, updatePlan,
  planAgent,
  createPlanTool,
} from '@crux/core/plan'
import type {
  Plan, PlanUpdate, CreatePlanInput,
  ToolDef, PlanAgent, PlanAgentOptions, PlanContextMode,
} from '@crux/core/plan'

Overview

Plans are freeform documents (title + content) that describe what an agent intends to do. They persist via CruxStore adapters and emit instrumentation events for devtools observability.

Plans model intent. Use them as a scratchpad for agent reasoning, a reviewable document for human approval, or a structured input for downstream task execution.

Task lists provide structured execution tracking and can be associated with a plan via planId. See Task Lists reference for the full task list API.

plan() and updatePlan() emit plan.operation spans with a JSON artifact containing the plan id, title, version, full content, content preview, and metadata. Crux devtools use that artifact as the authoritative Plans & Tasks read model, which keeps plan inspection working when the actual CruxStore is hosted inside Convex or another runtime boundary.

Why Plans?

Plans solve the "what should the agent do?" problem. Without plans, agent intent is scattered across conversation history, prompt instructions, and implicit assumptions. Plans give agents a dedicated, versioned document to describe their intent explicitly.

This matters for three reasons:

  1. Reviewability. When an agent writes a plan before acting, a human (or another agent) can review, approve, or reject it. This is the foundation of human-in-the-loop workflows.
  2. Reproducibility. Plans are versioned documents. If something goes wrong, you can inspect the exact plan the agent was following, at the exact version it was using.
  3. Composability. A plan created by an orchestrator can be read by worker agents, linked to task lists, and displayed in a reactive UI — all through the same CruxStore.

The most powerful way to use plans is to give an LLM a plan creation tool and let it decide what the plan should contain. See Agent Integration below.

Plans API

plan(input)

Create a new plan. Generates a UUID, sets version: 1, and persists immediately.

ParameterTypeDescription
inputCreatePlanInputPlan title, content, metadata

Returns: Promise<PlanHandle>

const p = await plan({
  title: 'Cloud Migration Guide',
  content: '## Objective\nWrite a comprehensive guide...',
})

getPlan(planId)

Get a plan by ID.

ParameterTypeDescription
planIdstringThe plan's ID

Returns: Promise<Plan | null>

updatePlan(planId, update)

Update a plan's fields. Version increments when title or content changes.

ParameterTypeDescription
planIdstringThe plan's ID
updatePlanUpdateFields to update (all optional)

Returns: Promise<Plan>

Throws: If the plan does not exist.

Version tracking lets you detect content drift. Editing the plan's content after initial creation increments the version, so downstream consumers can detect changes.

Context Injection

.asContext() returns a Crux Context — a composable fragment that injects a markdown section into the system message at generation time. When a prompt includes use: [planAgent.asContext()], the context resolver reads the plan from the store and renders it into the system message.

Full Mode (default)

In full mode, the entire plan content is injected. The rendered output looks like:

## Plan: Cloud Migration Guide (v1)
## Objective
Write a comprehensive guide covering lift-and-shift, re-platforming, and re-architecting strategies.

## Key Sections
1. Assessment framework
2. Migration patterns
3. Cost analysis

Use full mode for short plans (under ~1000 tokens) where the agent always needs the content.

Reference Mode

In reference mode, only metadata is injected, and the agent reads content on demand via the getPlan tool:

## Plan: Cloud Migration Guide (v3)
Use the `getPlan` tool to read the full content.

Use reference mode for long plans or when the agent may not need the content on every turn. This saves tokens — the agent only reads the plan when it actually needs it.

const planAgent = planAgent(p.id, { context: 'reference' })
prompt({
  use: [planAgent.asContext()],
  tools: { getPlan: planAgent.asTools().getPlan }, // agent reads on demand
})

Context priority controls ordering when multiple contexts compete for token budget. Plans default to priority 80 (high but below task worker assignments at 95). Override with planAgent.asContext({ priority: 60 }).

Overriding Defaults

Custom Context Rendering

Replace the default markdown rendering with renderContext:

const planAgent = planAgent(p.id, {
  renderContext: (plan) =>
    `<plan version="${plan.version}">\n${plan.content}\n</plan>`,
})

The callback receives the full Plan object and returns a string that becomes the context's system message contribution.

Custom Tool Descriptions

Each tool returned by .asTools() has a description property. To override it, wrap the tool:

const { getPlan, updatePlan } = planAgent.asTools()

const tools = {
  getPlan: {
    ...getPlan,
    description: 'Read the current project plan to understand what needs to be done next.',
  },
}

This is useful when the default description doesn't match your agent's domain language.

Agent Integration

Agent handles for plan interaction, plus a standalone creation tool. All follow the focused tools pattern — multiple dedicated tools instead of a single tool with an action parameter.

planAgent(planId, options?)

Create an agent handle for an existing plan. Provides context injection and tools for plan interaction.

ParameterTypeDescription
planIdstringThe plan's ID
optionsPlanAgentOptions?Context mode and render overrides

Returns: PlanAgent

MemberDescription
.planIdThe plan's ID (readonly)
.asContext(opts?)Returns a Context that injects plan content into the system message (default priority: 80)
.asTools()Returns { getPlan, updatePlan } — focused tools for plan interaction

PlanAgentOptions:

FieldTypeDefaultDescription
contextPlanContextMode'full''full' injects content, 'reference' injects metadata only (use with getPlan tool)
renderContext(plan: Plan) => stringOverride the default context rendering
const agent = planAgent(p.id)

// Context: inject plan into system message
prompt({ use: [agent.asContext()] })

// Tools: expose to LLM (pick which ones)
const { getPlan, updatePlan } = agent.asTools()
prompt({ tools: { getPlan } })  // read-only access

createPlanTool(options?)

Standalone tool for creating new plans. Use when no plan exists yet. Optionally accepts a template string that is included in the tool description to guide LLM output format.

ParameterTypeDescription
options{ template?: string }?Optional template for LLM guidance

Returns: ToolDef

// Basic — LLM decides plan structure
const tools = { createPlan: createPlanTool() }

// With template — LLM follows a specific format
const tools = {
  createPlan: createPlanTool({
    template: `## Goal\n[What we want to achieve]\n\n## Sections\n1. [Section title]\n2. [Section title]`,
  }),
}

Types

PlanHandle

Extends Plan with methods for mutation, context injection, and tool exposure. Returned by plan() and available via createPlanTool().created.

MethodReturnsDescription
update(update)Promise<Plan>Update the plan in the store
get()Promise<Plan | null>Re-read the latest plan from the store
asContext(options?)ContextCreate a Context for system message injection
asTools(){ getPlan, updatePlan }Returns focused tools for LLM agents

Plan

FieldTypeDescription
idstringUUID, generated by plan()
titlestringPlan title
contentstringFreeform content (markdown, prose, structured notes)
versionnumberIncrements on title/content changes only
metadataRecord<string, unknown>?Arbitrary user metadata
createdAtnumberUnix timestamp (ms)
updatedAtnumberUnix timestamp (ms)

ToolDef

The tool shape returned by all agent integration primitives. Compatible with AI SDK tool format.

interface ToolDef {
  description: string
  parameters: z.ZodType
  execute: (args: Record<string, unknown>) => Promise<string>
}

Input Types

interface CreatePlanInput {
  title: string
  content?: string
  metadata?: Record<string, unknown>
}

interface PlanUpdate {
  title?: string
  content?: string
  metadata?: Record<string, unknown>
}

Storage Keys

Plans use prefixed keys in the CruxStore:

EntityKey FormatExample
Planplan:{id}plan:a1b2c3d4

Usage Example

Give an LLM the plan creation tool and let it drive the process:

agent-driven-plan.ts
import { createPlanTool, planAgent } from '@crux/core/plan'
import { prompt, generate } from '@crux/ai'

// 1. Give the orchestrator a plan creation tool
const orchestratorPrompt = prompt({
  system: 'You are a content strategist. Create a plan for the requested article.',
  tools: { createPlan: createPlanTool({
    template: '## Goal\n[objective]\n\n## Sections\n1. [section]',
  }) },
})

const { toolCalls } = await generate(orchestratorPrompt, {
  model: myModel,
  input: { userRequest: 'Write a guide about TypeScript monorepos' },
})

const planId = toolCalls[0].result.id

// 2. Create agent handle for ongoing interaction
const planAgent = planAgent(planId)

// 3. Use in downstream prompts
prompt({
  use: [planAgent.asContext()],
  tools: planAgent.asTools(),
})

Foundation API — for cases where you create plans programmatically rather than through an LLM:

manual-plan.ts
import { plan, planAgent } from '@crux/core/plan'

const p = await plan({
  title: 'Write SEO Blog Post',
  content: '## Goal\nResearch and write a 1500-word blog post on TypeScript monorepos.',
})

const planAgent = planAgent(p.id)
prompt({
  use: [planAgent.asContext()],
  tools: planAgent.asTools(),
})

Real-World Example

A content writing pipeline where an orchestrator creates a plan, a reviewer approves it, and a writer follows it:

content-pipeline.ts
import { createPlanTool, planAgent } from '@crux/core/plan'
import { prompt, generate } from '@crux/ai'

// Phase 1: LLM creates a plan using the creation tool
const orchestratorPrompt = prompt({
  system: 'You are a content strategist. Create a plan for the requested article.',
  tools: { createPlan: createPlanTool() },
})

const { toolCalls } = await generate(orchestratorPrompt, {
  model: myModel,
  input: { userRequest: 'Write a guide about TypeScript monorepos' },
})

const planId = toolCalls[0].result.id

// Phase 2: Writer follows the plan
const planAgent = planAgent(planId)

const writerPrompt = prompt({
  system: 'You are a technical writer. Follow the plan to write the article.',
  use: [planAgent.asContext()],                    // plan injected into system message
  tools: { getPlan: planAgent.asTools().getPlan },  // can re-read for reference
})

await generate(writerPrompt, { model: myModel, input: {} })

Task lists can be associated with plans to drive structured execution. See the Task Lists reference for how to create a task list linked to a plan and use task worker agents.

On this page