Working memory asks what matters now. KV memory retrieves an exact fact. Vector memory finds relevant knowledge. Episodic memory recalls what happened before.

An operational agent still needs one more kind of memory:

How should this task be performed?

That is procedural memory: approved, reusable knowledge about how to act.

First: What Is Procedural Memory?

A procedure is a versioned playbook with ordered steps, preconditions, completion conditions, and escalation rules.

matching issue
      ↓
approved active procedure
      ↓
ordered application-controlled steps
      ↓
completed or escalated execution

It is different from remembering one past case.

EPISODIC                           PROCEDURAL

CASE-101                           FAILED_PAYMENT_PLAYBOOK v1
what happened?                     what should happen?
actions actually taken             approved ordered steps
case-specific outcome              reusable completion rules

Why Not Put Every Playbook in the System Prompt?

A large permanent prompt makes operational knowledge difficult to retrieve, approve, version, test, and audit.

Procedural memory keeps global rules and task-specific rules separate:

system instructions
→ invariants that always apply

procedural memory
→ approved process for one class of task

The application retrieves only the relevant active procedure. The model sees a smaller, explicit policy surface for the current task.

The Stored Procedure Is Structured

The procedure definition is not a paragraph that the model interprets freely:

@dataclass(frozen=True, slots=True)
class SupportProcedure:
    id: str
    procedure_key: str
    version: int
    name: str
    description: str
    category: str
    procedure_summary: str
    status: ProcedureStatus
    steps: tuple[ProcedureStep, ...]
    preconditions: tuple[str, ...]
    completion_conditions: tuple[str, ...]
    escalation_rules: tuple[str, ...]
    metadata: dict[str, JSONValue]

Each step has a sequence, action, optional tool, and required flag.

ProcedureStep(
    sequence=3,
    action="check_gateway",
    tool="check_gateway",
    required=True,
)

The JSONB steps are the execution source of truth.

Search Uses a Compact Procedure Summary

Embedding raw JSON would mix field names and storage syntax into the semantic representation. Instead, the manager builds readable search text:

Failed Payment Resolution.
Approved procedure for failed checkout payments.
Category: payments.
Use for: payment failed; checkout failed; card charged without order.
Important actions: identify transaction, check transaction,
check gateway, check refund, prevent duplicate refund, notify customer.
Escalate for: inconsistent transaction; gateway unavailable.

The summary finds the playbook. The structured row executes it.

issue embedding → ranked active procedures → exact JSONB steps

Only Active Procedures Are Eligible

A procedure version has one of three states:

DRAFT
ACTIVE
DEPRECATED

The lifecycle is explicit:

create v1 → draft → activate
                         ↓
create v2 → draft → activate v2
                         ↓
                v1 becomes deprecated

PostgreSQL enforces at most one active version for each procedure key with a partial unique index.

Activating a new version and deprecating the previous one happen inside a transaction. Only drafts may be deleted; active and deprecated versions remain auditable.

Retrieval Selects One Approved Playbook

search_procedures() embeds the issue and uses pgvector cosine distance. It can apply a category and minimum similarity threshold.

no candidate above threshold
→ normal support triage or escalation

one or more candidates
→ select the highest relevant active procedure

This learning project deliberately executes one procedure. Combining multiple playbooks would require conflict rules, dependency resolution, and a clearer approval model.

Execution Belongs to Application Code

After selection, ProcedureExecutor walks the stored steps in order.

load exact active procedure
       ↓
start execution record
       ↓
start step → run allowed action → persist result
       ↓
repeat
       ↓
complete or escalate

The model does not choose arbitrary SQL, reorder steps, activate a draft, or invent a tool name. Tool names come from a fixed allowlist.

Runtime State Is Separate From the Definition

The project stores executions independently:

support_procedures
→ immutable approved definition

procedure_executions
→ one run against exact procedure ID and version

procedure_step_executions
→ ordered status and result for each step

Executing a playbook never edits its definition. This separation makes it possible to answer both:

What was the approved process?
What actually happened during this run?

Safety Rules Are Enforced Below the Model

The deterministic support layer enforces several critical constraints:

  • only active procedures can execute
  • check_refund must precede issue_refund
  • refund issuance is idempotent
  • tool names come from an allowlist
  • unknown or inconsistent transaction state escalates
  • customer messages use current tool results
  • user text cannot administer procedure versions

These constraints remain in force even if model output is malformed or adversarial.

What Actually Happened?

I ran the real procedural manager, PostgreSQL store, pgvector retrieval, executor, and support tools against the isolated Docker test database. A deterministic test embedding provider made the trace reproducible.

The query selected the active failed-payment playbook and executed all six steps:

selected: FAILED_PAYMENT_PLAYBOOK v1 score=0.7263
execution: completed steps= 6

1. identify_transaction
   → {'transaction_id': 'TXN-DEMO-001', 'identified': True}

2. check_transaction
   → {'state': 'failed', 'amount': '49.00 USD',
      'order_created': False}

3. check_gateway
   → {'result': 'timeout', 'reference': 'gw-demo-1'}

4. check_refund
   → {'state': 'auto_refund_triggered',
      'eta_business_days': 5}

5. prevent_duplicate_refund
   → {'safe_to_issue': False,
      'refund_state': 'auto_refund_triggered'}

6. notify_customer
   → {'notified': True,
      'message': 'Transaction TXN-DEMO-001: refund status is
      auto_refund_triggered. Expected processing time: 5 business days.'}

outcome: procedure completion conditions satisfied
procedure unchanged: True

The executor did not issue another refund because current state showed that an automatic refund already existed.

The final equality check confirms that runtime execution left the approved definition unchanged.

Failure Becomes Escalation

The same executor has explicit terminal behavior for unsafe states.

missing transaction ID
→ escalate

unknown transaction state
→ escalate

gateway unavailable
→ escalate

tool failure
→ mark step failed and escalate

An escalation is a recorded outcome, not an exception that disappears from operational history.

The Model Writes the Grounded Answer Last

After deterministic execution, the Responses API receives:

selected procedure identity and version
completion and escalation conditions
available tool names
terminal execution state
current observations
current user issue

The model produces the natural-language response. It cannot claim that a step occurred unless the persisted execution marks it complete, and it must draw current facts from tool observations.

This places language generation after policy selection and execution rather than using language generation as the enforcement mechanism.

Procedures Should Not Learn Themselves

Successful episodes can reveal a better process, but they should not modify live policy automatically.

many episodes
     ↓
offline analysis
     ↓
human review
     ↓
new draft procedure version
     ↓
approval and activation

This preserves accountability. A production behavior change has an author, version, approval transition, and audit trail.

What the Tests Prove

I ran the full suite against the repository's local PostgreSQL/pgvector test database:

38 passed in 0.38s

The tests cover persistence, JSONB ordering, semantic retrieval, filters and thresholds, active-only selection, version transitions, uniqueness, rollback, execution state, step state, escalation, immutable definitions, duplicate-refund safety, response grounding, and the user/admin boundary.

Embeddings and support tools are deterministic in tests, and no OpenAI key is required.

The Five Memory Types Together

The projects now form a useful map:

Working memory
→ What is happening now?

KV memory
→ What exact fact do I know?

Vector memory
→ What knowledge is semantically relevant?

Episodic memory
→ What happened in a similar case?

Procedural memory
→ How should this task be performed?

They can coexist in one agent, but each needs its own write policy, retrieval contract, lifecycle, and trust boundary.

What I Took Away

Procedural memory turns operational knowledge into a controlled software artifact.

retrieve by meaning
approve and version explicitly
execute from structure
persist every run
generate language from observed results

The procedure guides action, application code enforces safety, and the model communicates the result. Keeping those responsibilities separate makes the system easier to test and audit.

Source Code

The complete project includes PostgreSQL migrations, pgvector retrieval, versioned procedures, deterministic execution, support tools, Responses API answer generation, Docker setup, a Rich terminal UI, and integration tests.

GITHUB / SOURCE CODEExplore the procedural-memory implementationsriraman-10 / applied-ai-concepts / memory / 05-procedural-memory

References

POSTGRESQL / VECTOR SEARCHpgvectorCosine-distance retrieval for active procedure summaries