logo
  • Docs
  • API Reference
    Introduction
    What is Lix?
    Getting Started
    Comparison to Git
    Lix for AI Agents
    Essentials
    How Lix Works
    Querying Changes
    Data Model
    Plugins
    Persistence
    Guides
    Versions (Branching)
    History
    Diffs
    Attribution (Blame)
    Change Proposals
    Validation Rules
    Undo/Redo
    Restore
    Conversations
    Labels
    Key-Value Store
    Environment API
    Testing
    React Integration
    Logging & Debugging
    Deterministic Mode
    Metadata
    Writer Key
    Architecture
    Lix as File Format
    Previous pageAttribution (Blame)Next pageValidation Rules

    #Change Proposals

    Change proposals enable review and approval workflows in your application. Users and AI agents can propose changes in isolated versions that are reviewed before merging into the main version.

    Change Proposals

    INFO

    Lix uses "change proposal" instead of "pull request" because many non-technical users aren't familiar with Git terminology. "Change proposal" clearly describes proposing changes for review.

    #How It Works

    A change proposal tracks two versions:

    • Source version - Contains the proposed changes
    • Target version - Where changes will be merged (usually "main")

    The proposal can be accepted (merging changes and deleting the source version) or rejected (marking the proposal without merging).

    #Schema

    Change proposals are simple - they just track the relationship between versions:

    {
      id: string;                    // Unique identifier
      source_version_id: string;     // Version with proposed changes
      target_version_id: string;     // Version to merge into
      status: "open" | "accepted" | "rejected";
    }

    Note: There are no built-in title, description, or comment fields. It's common to attach a conversation to a proposal for descriptions and comments, or store additional data as metadata in your own schema.

    #Examples

    #Create a change proposal

    // Get the main version
    const mainVersion = await lix.db
      .selectFrom("version")
      .where("name", "=", "main")
      .select(["id"])
      .executeTakeFirstOrThrow();
    
    // Create a feature version for proposed changes (inherits from main)
    const featureVersion = await createVersion({
      lix,
      name: "feature-typo-fixes",
      inheritsFrom: mainVersion,
    });
    
    // Switch to the feature version and make edits before proposing
    await switchVersion({ lix, to: featureVersion });
    // ...perform edits here (e.g., update files/entities)...
    
    // Create a change proposal
    const proposal = await createChangeProposal({
      lix,
      source: { id: featureVersion.id },
      target: { id: mainVersion.id },
    });
    
    console.log("Change proposal created:", proposal.id);
    console.log("Status:", proposal.status); // "open"

    What happens:

    1. A feature version is created from main
    2. Changes are made in the feature version
    3. A proposal is created linking source → target versions
    4. Proposal status is set to "open"

    #Query proposals

    // Get the main version
    const mainVersion = await lix.db
      .selectFrom("version")
      .where("name", "=", "main")
      .select(["id"])
      .executeTakeFirstOrThrow();
    
    // Create a feature version for proposed changes (inherits from main)
    const featureVersion = await createVersion({
      lix,
      name: "feature-typo-fixes",
      inheritsFrom: mainVersion,
    });
    
    // Switch to the feature version and make edits before proposing
    await switchVersion({ lix, to: featureVersion });
    // ...perform edits here (e.g., update files/entities)...
    
    // Create a change proposal
    const proposal = await createChangeProposal({
      lix,
      source: { id: featureVersion.id },
      target: { id: mainVersion.id },
    });
    
    console.log("Change proposal created:", proposal.id);
    console.log("Status:", proposal.status); // "open"
    // Query all open proposals
    const openProposals = await lix.db
      .selectFrom("change_proposal")
      .where("status", "=", "open")
      .selectAll()
      .execute();
    
    console.log("Open proposals:", openProposals.length);
    
    // Query proposals for a specific target version
    const mainProposals = await lix.db
      .selectFrom("change_proposal")
      .where("target_version_id", "=", mainVersion.id)
      .selectAll()
      .execute();
    
    console.log("Proposals targeting main:", mainProposals.length);

    Query proposals by status, target version, or any other criteria using standard SQL.

    #Accept a proposal

    // Get the main version
    const mainVersion = await lix.db
      .selectFrom("version")
      .where("name", "=", "main")
      .select(["id"])
      .executeTakeFirstOrThrow();
    
    // Create a feature version for proposed changes (inherits from main)
    const featureVersion = await createVersion({
      lix,
      name: "feature-typo-fixes",
      inheritsFrom: mainVersion,
    });
    
    // Switch to the feature version and make edits before proposing
    await switchVersion({ lix, to: featureVersion });
    // ...perform edits here (e.g., update files/entities)...
    
    // Create a change proposal
    const proposal = await createChangeProposal({
      lix,
      source: { id: featureVersion.id },
      target: { id: mainVersion.id },
    });
    
    console.log("Change proposal created:", proposal.id);
    console.log("Status:", proposal.status); // "open"
    
    // Query all open proposals
    const openProposals = await lix.db
      .selectFrom("change_proposal")
      .where("status", "=", "open")
      .selectAll()
      .execute();
    
    console.log("Open proposals:", openProposals.length);
    
    // Query proposals for a specific target version
    const mainProposals = await lix.db
      .selectFrom("change_proposal")
      .where("target_version_id", "=", mainVersion.id)
      .selectAll()
      .execute();
    
    console.log("Proposals targeting main:", mainProposals.length);
    // Switch back to main before accepting (so deleting the source version is safe)
    await switchVersion({ lix, to: mainVersion });
    
    // Accept the proposal - this merges changes and deletes the source version
    await acceptChangeProposal({
      lix,
      proposal: { id: proposal.id },
    });
    
    console.log("Proposal accepted and merged");
    
    // Verify the source version was deleted after acceptance
    const acceptedSource = await lix.db
      .selectFrom("version")
      .where("id", "=", featureVersion.id)
      .selectAll()
      .executeTakeFirst();
    
    console.log("Source version deleted:", !acceptedSource);

    What happens:

    1. Changes from source version are merged into target version
    2. Proposal status is set to "accepted"
    3. Source version is deleted (changes are now in target)

    #Reject a proposal

    // Get the main version
    const mainVersion = await lix.db
      .selectFrom("version")
      .where("name", "=", "main")
      .select(["id"])
      .executeTakeFirstOrThrow();
    
    // Create a feature version for proposed changes (inherits from main)
    const featureVersion = await createVersion({
      lix,
      name: "feature-typo-fixes",
      inheritsFrom: mainVersion,
    });
    
    // Switch to the feature version and make edits before proposing
    await switchVersion({ lix, to: featureVersion });
    // ...perform edits here (e.g., update files/entities)...
    
    // Create a change proposal
    const proposal = await createChangeProposal({
      lix,
      source: { id: featureVersion.id },
      target: { id: mainVersion.id },
    });
    
    console.log("Change proposal created:", proposal.id);
    console.log("Status:", proposal.status); // "open"
    
    // Query all open proposals
    const openProposals = await lix.db
      .selectFrom("change_proposal")
      .where("status", "=", "open")
      .selectAll()
      .execute();
    
    console.log("Open proposals:", openProposals.length);
    
    // Query proposals for a specific target version
    const mainProposals = await lix.db
      .selectFrom("change_proposal")
      .where("target_version_id", "=", mainVersion.id)
      .selectAll()
      .execute();
    
    console.log("Proposals targeting main:", mainProposals.length);
    
    // Switch back to main before accepting (so deleting the source version is safe)
    await switchVersion({ lix, to: mainVersion });
    
    // Accept the proposal - this merges changes and deletes the source version
    await acceptChangeProposal({
      lix,
      proposal: { id: proposal.id },
    });
    
    console.log("Proposal accepted and merged");
    
    // Verify the source version was deleted after acceptance
    const acceptedSource = await lix.db
      .selectFrom("version")
      .where("id", "=", featureVersion.id)
      .selectAll()
      .executeTakeFirst();
    
    console.log("Source version deleted:", !acceptedSource);
    // Create another proposal to demonstrate rejection
    const featureVersion2 = await createVersion({
      lix,
      name: "feature-experimental",
      inheritsFrom: mainVersion,
    });
    
    const proposal2 = await createChangeProposal({
      lix,
      source: { id: featureVersion2.id },
      target: { id: mainVersion.id },
    });
    
    // Reject the proposal - marks as rejected, keeps source version
    await rejectChangeProposal({
      lix,
      proposal: { id: proposal2.id },
    });
    
    console.log("Proposal rejected");
    
    // Verify the rejection
    const rejectedProposal = await lix.db
      .selectFrom("change_proposal")
      .where("id", "=", proposal2.id)
      .selectAll()
      .executeTakeFirst();
    
    console.log("Proposal status:", rejectedProposal?.status); // "rejected"
    
    // Source version still exists after rejection
    const sourceExists = await lix.db
      .selectFrom("version")
      .where("id", "=", featureVersion2.id)
      .selectAll()
      .executeTakeFirst();
    
    console.log("Source version still exists:", !!sourceExists);

    What happens:

    1. Proposal status is set to "rejected"
    2. Source version remains (can be modified or deleted manually)

    #Use Cases

    User collaboration

    • Users propose changes for review
    • Reviewers accept or reject proposals
    • History of all proposals is preserved

    AI agent safety

    • AI proposes changes instead of directly modifying
    • User reviews AI suggestions before applying
    • Failed proposals can be retried with feedback

    Approval workflows

    • Require approval before changes go live
    • Track who approved what and when
    • Audit trail of all change proposals

    #Building a review UI

    To build a full review UI, you'll likely want to:

    1. Add your own metadata:

    // Store title, description, etc. in your own schema
    await lix.db.insertInto("proposal_metadata").values({
      proposal_id: proposal.id,
      title: "Fix typos in documentation",
      description: "Corrects spelling errors in user guide",
      author_id: currentUser.id,
    });

    2. Show diffs:

    // Compare source and target versions to show changes
    const diff = await lix.db
      .selectFrom("file")
      .where("lixcol_version_id", "=", proposal.source_version_id)
      // ... compare with target version
      .execute();

    3. Add comments/discussions:

    • Store comments that reference proposal ID
    • Use Lix's built-in conversations feature

    4. Handle rejection reasons:

    • Store rejection reasons in your own metadata
    • Link them to the proposal ID

    #Agent SDK Integration

    The @lix-js/agent-sdk provides higher-level abstractions:

    • Auto-accept mode - Skip proposals when user trusts the agent
    • Proposal rejection errors - Structured error handling
    • Proposal lifecycle hooks - React to proposal events

    See the agent SDK documentation for details.

    #Next Steps

    • Learn about Versions to understand source and target versions
    • Explore Conversations for adding discussion to proposals
    • See Metadata for storing additional proposal information