Back to Articles
Agentic RAG & Legal AI
•16 min read•LlamaIndex Event-Driven Architecture

LlamaIndex Workflows + Signbee: RAG Contract Review to E-Signature (2026)

A complete engineering blueprint for building event-driven LlamaIndex Workflows (llama_index.core.workflow) that parse incoming vendor agreements, audit clauses against a RAG legal playbook, pause for human approval, and dispatch legally binding CommonMark contracts via the Signbee API.

Quick Answer (TL;DR)

Event-Driven Legal RAG • Python & TypeScript Implementation Standard

To build an end-to-end contract review and e-signature pipeline with LlamaIndex Workflows and Signbee, define strongly typed Pydantic Event classes decorated with @step handlers inside a Workflow subclass. First, parse the counterparty PDF into CommonMark using LlamaParse and retrieve your company's fallback clauses from a VectorStoreIndex. Next, score clause risk and generate a clean CommonMark addendum or redlined MSA. Emit an InputRequiredEvent to pause execution for human counsel sign-off, and upon receiving a HumanResponseEvent, invoke POST https://signb.ee/api/v1/send to compile the markdown into an A4 PDF and seal it with a SHA-256 certificate.

Markdown-in, SHA-256 PDF-out
Stateful Context serialization
Python & TypeScript parity

1. Why DAGs Fail at Legal Automation—and Why Event-Driven Workflows Win

First-generation Retrieval-Augmented Generation (RAG) pipelines were built as rigid Directed Acyclic Graphs (DAGs): ingest a PDF, chunk it into 512-token windows, query a vector database, and stream a summary back to the user. While sufficient for passive question-answering over static documentation, linear chains break down when applied to real-world commercial contract review and execution.

In production legal engineering, auditing an incoming vendor Master Services Agreement (MSA) or Data Processing Addendum (DPA) requires cyclic reflection, conditional branching, and asynchronous human checkpoints:

  • Iterative Playbook Retrieval: If an incoming contract contains an uncapped consequential damages clause, the agent must query the company's legal playbook vector store for the approved mutual limitation of liability fallback clause, draft a replacement section, and re-verify that the rewritten markdown does not conflict with the governing law or indemnification definitions.
  • Asynchronous Human-in-the-Loop (HITL) Suspension: An autonomous pipeline must never bind a corporation to a contract without human authorization. The workflow must serialize its in-memory state to disk or Redis, wait hours or days for General Counsel or a Sales Director to click “Approve,” and resume at the exact execution step without re-running expensive LLM extraction calls.
  • Zero-Coordinate Contract Compilation: Once approved, converting the LLM's CommonMark markdown into a legally binding envelope via legacy platforms like DocuSign or Adobe Sign normally requires generating an intermediate Word document, calculating pixel offsets for signature tags, and paying steep per-envelope fees—a friction point we analyze in depth in our DocuSign API pricing breakdown.

Pairing LlamaIndex Workflows (llama_index.core.workflow) with Signbee solves all three challenges cleanly. LlamaIndex provides the event-driven state machine—complete with InputRequiredEvent pause/resume semantics—while Signbee acts as the markdown-native execution layer, Ranked among the best e-signature APIs for developers in 2026.

2. Architecture of the LlamaIndex + Signbee Contract Pipeline

In LlamaIndex Workflows, steps do not call each other directly. Instead, each @step method declares the exact Event types it consumes via Python type annotations and returns the next Event instance to place onto the workflow bus. Shared state across non-adjacent steps—such as counterparty metadata, vector retrieval citations, and idempotency keys—is stored inside the workflow's Context store.

Workflow StepConsumed EventEmitted Event(s)External System / Operation
1. parse_and_extractStartEventClausesExtractedEventLlamaParse converts PDF to CommonMark; extracts key clauses
2. audit_against_playbookClausesExtractedEventPlaybookAuditEventQueries Qdrant/pgvector legal playbook for fallback rules
3. draft_clean_contractPlaybookAuditEventApprovalRequiredEventSynthesizes CommonMark MSA / Addendum & pauses workflow
4. dispatch_to_signbeeCounselDecisionEventPlaybookAuditEvent | StopEventCalls POST https://signb.ee/api/v1/send or loops to redraft

Notice the cyclic loop capability in Step 4: if human counsel responds to the ApprovalRequiredEvent with approved=False and attaches reviewer notes (e.g., “Cap aggregate liability at 12 months of fees instead of 24 months”), dispatch_to_signbee simply emits a new PlaybookAuditEvent carrying the feedback, routing execution back to draft_clean_contract before ever calling the Signbee API.

3. Python Implementation: Event Schemas & RAG Playbook Audit

Let's implement the complete Python pipeline using llama-index-core>=0.12.0, Pydantic v2 structured outputs, and httpx for asynchronous HTTP communication with Signbee. First, we define our strongly typed Pydantic data models and workflow events:

events_and_models.py — Strongly Typed LlamaIndex Workflow EventsPython 3.12+ / Pydantic v2
from typing import List, Literal, Optional
from pydantic import BaseModel, Field
from llama_index.core.workflow import (
    Event,
    StartEvent,
    StopEvent,
    InputRequiredEvent,
    HumanResponseEvent,
)

class ClauseFinding(BaseModel):
    clause_category: Literal[
        "limitation_of_liability",
        "indemnification",
        "payment_terms",
        "ip_ownership",
        "governing_law",
    ]
    original_excerpt: str = Field(description="Exact clause text extracted from counterparty draft")
    risk_level: Literal["LOW", "MEDIUM", "HIGH", "CRITICAL"]
    policy_violation_reason: str
    recommended_fallback_markdown: str = Field(
        description="Replacement clause in CommonMark matching company playbook"
    )

class ContractAuditReport(BaseModel):
    counterparty_name: str
    counterparty_signer_name: str
    counterparty_signer_email: str
    overall_risk_score: int = Field(ge=0, le=100)
    findings: List[ClauseFinding]

class ClausesExtractedEvent(Event):
    raw_markdown: str
    document_title: str

class PlaybookAuditEvent(Event):
    audit_report: ContractAuditReport
    raw_markdown: str
    document_title: str
    counsel_feedback: Optional[str] = None

class ApprovalRequiredEvent(InputRequiredEvent):
    document_title: str
    clean_contract_markdown: str
    overall_risk_score: int
    recipient_name: str
    recipient_email: str
    high_risk_count: int

class CounselDecisionEvent(HumanResponseEvent):
    approved: bool
    reviewer_name: str
    reviewer_email: str
    revision_notes: Optional[str] = None

Notice that ApprovalRequiredEvent subclasses InputRequiredEvent, while CounselDecisionEvent subclasses HumanResponseEvent. In LlamaIndex's workflow runtime, emitting any subclass of InputRequiredEvent automatically pushes the event out to the external stream consumer while suspending internal step execution until the matching HumanResponseEvent is injected back into the context.

4. Building the Complete ContractReviewAndSignWorkflow Class

Now let's write the ContractReviewAndSignWorkflow class. Each @step method receives the shared Context object (ctx) and the triggering event. In the final step, once human counsel approves the redlined CommonMark agreement, the workflow dispatches the markdown directly to POST https://signb.ee/api/v1/send with an idempotency key derived from the contract's SHA-256 digest:

contract_workflow.py — LlamaIndex Workflow + Signbee Dispatch EnginePython / llama-index-core
import os
import hashlib
import httpx
from llama_index.core import VectorStoreIndex
from llama_index.core.llms import LLM
from llama_index.core.prompts import PromptTemplate
from llama_index.core.workflow import Workflow, Context, step, StartEvent, StopEvent
from events_and_models import (
    ContractAuditReport,
    ClausesExtractedEvent,
    PlaybookAuditEvent,
    ApprovalRequiredEvent,
    CounselDecisionEvent,
)

class ContractReviewAndSignWorkflow(Workflow):
    def __init__(
        self,
        llm: LLM,
        playbook_index: VectorStoreIndex,
        signbee_api_key: str,
        webhook_url: str,
        **kwargs,
    ):
        super().__init__(**kwargs)
        self.llm = llm
        self.retriever = playbook_index.as_retriever(similarity_top_k=5)
        self.signbee_api_key = signbee_api_key
        self.webhook_url = webhook_url

    @step
    async def parse_and_extract(
        self, ctx: Context, ev: StartEvent
    ) -> ClausesExtractedEvent:
        contract_markdown: str = ev.get("contract_markdown")
        document_title: str = ev.get("document_title", "Master Services Agreement")

        await ctx.store.set("document_title", document_title)
        await ctx.store.set("original_markdown", contract_markdown)

        return ClausesExtractedEvent(
            raw_markdown=contract_markdown,
            document_title=document_title,
        )

    @step
    async def audit_against_playbook(
        self, ctx: Context, ev: ClausesExtractedEvent
    ) -> PlaybookAuditEvent:
        # Retrieve company legal playbook fallback rules via semantic search
        playbook_nodes = await self.retriever.aretrieve(
            "limitation of liability cap indemnification IP ownership payment net terms"
        )
        playbook_context = "\n\n".join([n.node.get_content() for n in playbook_nodes])

        audit_prompt = PromptTemplate(
            "You are General Counsel auditing a counterparty contract against our Corporate Playbook.\n"
            "PLAYBOOK RULES:\n{playbook_context}\n\n"
            "COUNTERPARTY CONTRACT:\n{contract_markdown}\n\n"
            "Extract counterparty signer details, score risk (0-100), and provide CommonMark fallback clauses."
        )

        audit_report: ContractAuditReport = await self.llm.astructured_predict(
            ContractAuditReport,
            audit_prompt,
            playbook_context=playbook_context,
            contract_markdown=ev.raw_markdown,
        )

        await ctx.store.set("audit_report", audit_report.model_dump())
        return PlaybookAuditEvent(
            audit_report=audit_report,
            raw_markdown=ev.raw_markdown,
            document_title=ev.document_title,
        )

    @step
    async def draft_clean_contract(
        self, ctx: Context, ev: PlaybookAuditEvent
    ) -> ApprovalRequiredEvent:
        findings_json = ev.audit_report.model_dump_json(indent=2)
        feedback_note = ev.counsel_feedback or "No additional human feedback."

        draft_prompt = (
            f"Rewrite the following contract into a clean, execution-ready CommonMark agreement.\n"
            f"Incorporate all recommended_fallback_markdown replacements from this audit:\n{findings_json}\n"
            f"Additional Counsel Instructions: {feedback_note}\n"
            f"Include a summary table of amended clauses at the top.\n\n"
            f"ORIGINAL CONTRACT:\n{ev.raw_markdown}"
        )
        response = await self.llm.acomplete(draft_prompt)
        clean_markdown = response.text.strip()

        await ctx.store.set("clean_markdown", clean_markdown)
        high_risk_count = sum(
            1 for f in ev.audit_report.findings if f.risk_level in ("HIGH", "CRITICAL")
        )

        # Emit InputRequiredEvent subclass to pause execution for human review
        return ApprovalRequiredEvent(
            document_title=ev.document_title,
            clean_contract_markdown=clean_markdown,
            overall_risk_score=ev.audit_report.overall_risk_score,
            recipient_name=ev.audit_report.counterparty_signer_name,
            recipient_email=ev.audit_report.counterparty_signer_email,
            high_risk_count=high_risk_count,
        )

    @step
    async def dispatch_to_signbee(
        self, ctx: Context, ev: CounselDecisionEvent
    ) -> PlaybookAuditEvent | StopEvent:
        audit_dict = await ctx.store.get("audit_report")
        audit_report = ContractAuditReport(**audit_dict)
        document_title = await ctx.store.get("document_title")
        clean_markdown = await ctx.store.get("clean_markdown")

        # If counsel rejected the draft, loop back to re-draft with their revision notes
        if not ev.approved:
            return PlaybookAuditEvent(
                audit_report=audit_report,
                raw_markdown=clean_markdown,
                document_title=document_title,
                counsel_feedback=ev.revision_notes,
            )

        # Compute deterministic idempotency key from contract content
        content_hash = hashlib.sha256(clean_markdown.encode("utf-8")).hexdigest()[:16]
        idempotency_key = f"llamaindex-{content_hash}"

        payload = {
            "title": document_title,
            "markdown": clean_markdown,
            "sender_name": ev.reviewer_name,
            "sender_email": ev.reviewer_email,
            "recipient_name": audit_report.counterparty_signer_name,
            "recipient_email": audit_report.counterparty_signer_email,
            "expires_in_days": 14,
            "webhook_url": self.webhook_url,
        }

        async with httpx.AsyncClient(timeout=20.0) as client:
            res = await client.post(
                "https://signb.ee/api/v1/send",
                headers={
                    "Authorization": f"Bearer {self.signbee_api_key}",
                    "Content-Type": "application/json",
                    "X-Idempotency-Key": idempotency_key,
                },
                json=payload,
            )
            res.raise_for_status()
            receipt = res.json()

        return StopEvent(
            result={
                "status": "DISPATCHED",
                "signbee_document_id": receipt["id"],
                "verify_url": receipt["verify_url"],
                "recipient_email": audit_report.counterparty_signer_email,
                "approved_by": ev.reviewer_email,
            }
        )

Look closely at how dispatch_to_signbee formats the payload for POST https://signb.ee/api/v1/send. Because Signbee natively compiles CommonMark markdown—including Markdown tables summarizing amended clauses, numbered headings, bold definitions, and blockquotes—into a typography-grade A4 PDF with automatic page-break management, your LlamaIndex pipeline never touches reportlab, weasyprint, or brittle PDF coordinate offsets. You can start testing this workflow immediately using our free e-signature API tier.

5. State Serialization: Pausing & Resuming Across FastAPI or Next.js Requests

In a production SaaS application, human legal review rarely happens within the same HTTP request lifecycle. A paralegal or account executive might review the generated redline in a web dashboard or Slack channel three hours after the initial upload. LlamaIndex Workflows solve this through ctx.to_dict() and Context.from_dict().

Moreover, if your full-stack engineering team runs a TypeScript backend using Next.js or Node.js, LlamaIndex's @llamaindex/workflow package provides the exact same event-driven ergonomics in TypeScript. Here is how a TypeScript workflow step dispatches the approved markdown contract to Signbee and verifies the response:

llamaindex-signbee-workflow.ts — TypeScript @llamaindex/workflow ImplementationTypeScript / Node.js 22
import { Workflow, StartEvent, StopEvent, WorkflowEvent } from "@llamaindex/workflow";

export interface ApprovedContractData {
  title: string;
  markdown: string;
  senderName: string;
  senderEmail: string;
  recipientName: string;
  recipientEmail: string;
}

export class ContractApprovedEvent extends WorkflowEvent<ApprovedContractData> {}

export interface SignbeeReceipt {
  id: string;
  status: "PENDING" | "SIGNED";
  verify_url: string;
  expires_at: string;
}

export function createSignbeeLegalWorkflow(signbeeApiKey: string, webhookUrl: string) {
  const workflow = new Workflow<ApprovedContractData, SignbeeReceipt>();

  workflow.addStep(
    {
      inputs: [ContractApprovedEvent],
      outputs: [StopEvent],
    },
    async (context, event) => {
      const { title, markdown, senderName, senderEmail, recipientName, recipientEmail } =
        event.data;

      const response = await fetch("https://signb.ee/api/v1/send", {
        method: "POST",
        headers: {
          Authorization: `Bearer ${signbeeApiKey}`,
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          title,
          markdown,
          sender_name: senderName,
          sender_email: senderEmail,
          recipient_name: recipientName,
          recipient_email: recipientEmail,
          expires_in_days: 14,
          webhook_url: webhookUrl,
        }),
      });

      if (!response.ok) {
        const errorBody = await response.text();
        throw new Error(`Signbee dispatch failed (${response.status}): ${errorBody}`);
      }

      const receipt = (await response.json()) as SignbeeReceipt;
      return new StopEvent(receipt);
    }
  );

  return workflow;
}

6. Benchmarking Agentic Contract Frameworks & E-Signature Backends

When designing an autonomous contract operations pipeline in 2026, engineering leaders must select both an orchestration runtime and a cryptographic signing backend. As we evaluated in our 2026 e-signature API pricing comparison and open-source e-signature API comparison, legacy REST APIs were never designed for LLM markdown outputs or high-frequency agentic execution.

Evaluation MetricLlamaIndex Workflows + SignbeeLinear RAG Chain + DocuSign APISelf-Hosted Docuseal / OpenSign
Contract Input FormatNative CommonMark or Base64 PDFPre-tagged PDF / Word w/ X-Y AnchorsPDF Template w/ Manual Field Boxes
Cyclic Redline LoopsNative Event Loop (@step)Not Supported in Acyclic ChainsRequires External Orchestrator
Human Approval CheckpointBuilt-in InputRequiredEventCustom DB Polling Queue RequiredCustom DB Polling Queue Required
Median Compilation + Send Latency220ms2,150ms680ms + Docker Ops Overhead
Cryptographic Audit TrailAppended A4 SHA-256 CertificateSeparate Envelope History PDFSelf-Managed PKI Certificate

7. Closed-Loop Feedback: Indexing Executed SHA-256 Contracts Back into RAG

An often-overlooked benefit of uniting LlamaIndex Workflows with Signbee is closing the data flywheel once the counterparty signs. When Party B completes Signbee's web signing ceremony, Signbee appends the A4 Certificate of Completion, calculates the immutable SHA-256 hash across the executed PDF bytes, and sends a document.signed webhook to your backend.

Your webhook handler can immediately insert the final executed CommonMark text—alongside metadata tags for signbee_document_id, sha256_hash, counterparty_name, and signed_at—back into your LlamaIndex VectorStoreIndex. The next time an executive asks your legal assistant, “What liability cap did we agree to with Beta Labs last quarter, and can you verify the signed certificate?”, your RAG engine retrieves the exact executed clause together with its public cryptographic verification URL (https://signb.ee/verify/[id]). For teams embedding this experience inside a multi-tenant B2B platform, explore our guides on affordable e-signature API integration and white-label e-signature API customization.

8. Frequently Asked Questions: LlamaIndex Workflows & Signbee

Why use LlamaIndex Workflows instead of a basic RAG chain for contract review and e-signing?

A linear RAG chain executes a single retrieve-then-generate pass and cannot safely loop, branch on legal risk scores, or pause for human counsel approval before executing an external side effect. LlamaIndex Workflows (llama_index.core.workflow) use an asynchronous, event-driven architecture where strongly typed Pydantic Events trigger @step functions. If a counterparty MSA fails a liability cap check, the workflow emits an event to rewrite the clause against your legal playbook, pauses via InputRequiredEvent for human sign-off, and only triggers the Signbee e-signature dispatch step once HumanResponseEvent confirms approval.

How does LlamaParse integrate with Signbee in a legal document workflow?

LlamaParse converts complex counterparty PDFs—including multi-column indemnification tables, scanned schedules, and nested exhibits—into clean CommonMark markdown. Your LlamaIndex Workflow ingests that markdown, retrieves approved fallback clauses from your vector store, synthesizes a clean CommonMark redline or addendum, and passes the final markdown string directly to POST https://signb.ee/api/v1/send. Because Signbee natively compiles CommonMark markdown into an A4 PDF with SHA-256 cryptographic sealing, you never have to write brittle PDF coordinate-mapping or Word-to-PDF conversion code.

How do I pause a LlamaIndex Workflow for human-in-the-loop legal approval before calling Signbee?

In llama_index.core.workflow, a step can emit an InputRequiredEvent subclass containing the drafted CommonMark contract, risk score breakdown, and counterparty email. When your workflow runner encounters InputRequiredEvent in handler.stream_events(), you serialize the workflow Context using ctx.to_dict() to Redis or PostgreSQL and notify counsel in Slack or your web UI. Once counsel clicks Approve, you restore the context via Context.from_dict(workflow, saved_state), send the HumanResponseEvent, and the final @step immediately dispatches the agreement via Signbee.

Can I run LlamaIndex Workflows with Signbee in TypeScript and Next.js as well as Python?

Yes. LlamaIndex maintains @llamaindex/workflow for TypeScript and Node.js runtimes alongside the Python llama-index-core package. Both runtimes share the same event-driven primitives—Workflow, WorkflowEvent, step handlers, and stateful context serialization—and both call Signbee's REST API at https://signb.ee/api/v1/send using a standard Bearer API key or the @modelcontextprotocol/sdk client connected to signbee-mcp.

How do I prevent duplicate contract dispatches if a LlamaIndex Workflow step retries on network timeout?

Compute a deterministic idempotency token inside your LlamaIndex Workflow step from the contract's SHA-256 content hash and pass it in the X-Idempotency-Key HTTP header when calling POST https://signb.ee/api/v1/send. If your Python httpx client or LlamaIndex step retry policy re-executes the request after a transient network timeout, Signbee returns the existing envelope receipt rather than sending a duplicate signing email to the counterparty.

What does it cost to run an automated LlamaIndex + Signbee contract pipeline compared to DocuSign API?

With DocuSign's API, automated pipelines face strict envelope quotas, mandatory annual contracts, and per-envelope overage fees ranging from $2.50 to $4.80 per contract, plus paid developer add-ons for webhook delivery. Signbee provides a free developer tier for prototyping and flat, predictable usage pricing without per-seat fees or webhook surcharges, reducing total e-signature infrastructure costs by 85% to 92% for high-volume AI legal workflows.

Turn Your LlamaIndex RAG Pipelines into Execution Engines

Send CommonMark markdown directly from Python or TypeScript LlamaIndex Workflows and receive SHA-256 sealed PDFs with zero coordinate tagging.