Document processing is one of the most common enterprise AI use cases. Every organization has documents that need to be read, analyzed, and transformed into structured reports -- compliance reviews, financial summaries, research digests, contract analyses. The challenge is not building a demo that works once; it is building a system that works reliably in production, with human oversight, real-time feedback, and full observability.
This tutorial walks through building a complete document-to-report pipeline using FireFoundry. You will upload a document, extract its content, analyze it with an LLM, generate a structured report, run it through a human review cycle, and produce a final PDF. The full tutorial is a 13-part series that takes you from your first entity to a deployed consumer application. This article gives you the high-level walkthrough so you understand the architecture before diving into the code.
What We're Building
The finished application is a document-to-report generator with these capabilities:
- 1 Document upload -- accepts PDFs, Word files, and plain text through a REST API
- 2 Content extraction -- uses OCR and structural parsing to pull text from uploaded files
- 3 AI analysis -- an LLM processes the extracted content and generates a structured HTML report
- 4 Human review -- a reviewer approves the report or provides feedback for revision
- 5 Iterative refinement -- the LLM incorporates feedback and produces updated versions
- 6 PDF generation -- the final approved report is converted to a downloadable PDF
All of this runs on FireFoundry's infrastructure with persistent state, real-time progress streaming, and full telemetry -- the same qualities you need for any production AI system.
Key Concepts
Before walking through the pipeline, here are the FireFoundry concepts that make this architecture possible.
Entities and the Entity Graph
Entities are persistent business objects stored in FireFoundry's entity graph. In this pipeline, we use a ReviewableEntity for documents that need human approval and RunnableEntity for workflow orchestration. Each entity has typed data, a lifecycle status, and edges connecting it to related entities. The graph persists across restarts -- nothing is lost.
Bots and Multi-Bot Pipelines
Bots are stateless AI behaviors that take input and produce output. The pipeline uses multiple specialized bots: a ReportGenerationBot that generates structured HTML from document text, and a FeedbackBotMixin that incorporates reviewer comments into revised output. Bots use Zod schemas for validated structured output, so the LLM always returns data in the shape your code expects.
Document Processing Service
FireFoundry's doc-proc-client handles OCR and content extraction from uploaded files. It also converts HTML to PDF with configurable page orientation. You call it as a service -- no need to manage Tesseract, Puppeteer, or PDF libraries yourself.
Working Memory
Working Memory provides file and blob storage for documents and generated reports. When a user uploads a PDF, it goes into Working Memory. When the pipeline produces a final PDF, it is stored there too. The WorkingMemoryProvider gives entities a clean API for storing and retrieving binary content alongside their structured data.
Progress Streaming
Every stage of the pipeline yields progress envelopes -- typed events like INTERNAL_UPDATE, VALUE, and WAITING. These are streamed to the consumer application via Server-Sent Events (SSE), giving users real-time visibility into what the system is doing. No polling required.
Human-in-the-Loop Review
The ReviewableEntity pattern pauses the workflow after report generation and creates a ReviewStep entity. A human reviewer can approve the report, reject it, or provide specific feedback. On rejection with feedback, the pipeline automatically re-runs with the reviewer's comments injected into the LLM prompt, producing a revised version. This cycle can repeat until the reviewer is satisfied.
The Pipeline: Step by Step
Here is the full flow from document upload to final PDF. Each step maps to one or more parts of the 13-part tutorial.
Upload Document
The user uploads a document (PDF, DOCX, or plain text) through a REST API endpoint. The consumer backend proxies the upload to the agent bundle, which stores the file in Working Memory and creates a ReportReviewWorkflowEntity to manage the pipeline. The entity ID is returned to the client immediately, and the workflow begins running in the background.
Extract Text and Structure
The doc-proc-client extracts text content from the uploaded file. For PDFs, this includes OCR for scanned documents. For Word files, it parses the document structure. The extracted plain text is stored back in Working Memory and made available to the next stage.
Analyze Content with AI
The ReportGenerationBot receives the extracted text along with a user-provided prompt (such as "Summarize the key findings" or "Create a compliance review"). The bot uses a composed prompt with configurable sections and returns validated structured output -- a reasoning trace and the HTML report content.
Generate Structured Report
The bot's HTML output is validated against a Zod schema to ensure it contains the expected fields. The HTML report is then converted to PDF using the doc-proc service with configurable page orientation (portrait or landscape). Both the HTML and PDF are stored in Working Memory.
Human Review
The workflow pauses and creates a ReviewStep entity. The progress stream sends a WAITING envelope to the client, which displays the generated report alongside approve and reject buttons. The reviewer reads the report and decides whether it meets their requirements.
Incorporate Feedback
If the reviewer rejects the report with feedback, the ReviewableEntity increments the version, stores the feedback in the entity's config column, and re-runs the pipeline. The FeedbackBotMixin automatically injects the reviewer's comments and the previous result into the LLM prompt, producing a targeted revision rather than a from-scratch rewrite.
Final PDF Output
Once the reviewer approves, the workflow completes. The final PDF is available for download from Working Memory via the consumer backend. The entire history -- every version, every piece of feedback, every LLM call -- is persisted in the entity graph and visible through FireFoundry's telemetry tools.
Code Highlights
Here are three code patterns from the tutorial that illustrate how FireFoundry's architecture keeps things clean.
Entity with ReviewableEntity
The top-level workflow entity extends ReviewableEntity, which handles the entire review loop. You provide the wrapped entity class, and the framework manages creating review steps, storing feedback, and re-running on rejection.
@EntityMixin({
specificType: 'ReportReviewWorkflowEntity',
generalType: 'ReportReviewWorkflowEntity',
allowedConnections: {}
})
export class ReportReviewWorkflowEntity extends ReviewableEntity<
ReportReviewWorkflowRETH
> {
// Tell the framework which entity to run inside the review loop
protected override get wrappedEntityClass() {
return 'ReportEntity';
}
// Configure the review step prompt
protected override get reviewPrompt(): string {
return 'Please review the generated report. Approve if it meets ' +
'your requirements, or provide feedback for revision.';
}
// Extract the result to show the reviewer
protected override createResultEntity(
result: REPORT_WORKFLOW_OUTPUT
) {
return {
html_content: result.html_content,
pdf_working_memory_id: result.pdf_working_memory_id
};
}
}
Bot with Structured Output Validation
Bots use Zod schemas with withSchemaMetadata to define exactly what the LLM should return. The StructuredOutputBotMixin automatically injects the schema into the prompt and validates the response. Field ordering matters -- placing reasoning before html_content forces the LLM to plan before generating.
import { z } from 'zod';
import { withSchemaMetadata } from '@firebrandanalytics/ff-agent-sdk';
export const ReportOutputSchema = withSchemaMetadata(
z.object({
reasoning: z.string()
.describe('Your thought process for structuring this report. '
+ 'Explain what sections you chose and why.'),
html_content: z.string()
.describe('Complete HTML document with embedded CSS styling. '
+ 'Must include DOCTYPE, html, head with style tag, and body.')
}),
'Your final output',
'AI-generated HTML report with reasoning'
);
export type REPORT_OUTPUT = z.infer<typeof ReportOutputSchema>;
SSE Progress Streaming Endpoint
The consumer backend bridges the agent bundle's async iterator to the browser via Server-Sent Events. The client connects once and receives typed progress envelopes in real time -- no polling, no WebSocket complexity.
export async function GET(request: NextRequest, { params }) {
const { id: entityId } = await params;
const client = new RemoteAgentBundleClient(AGENT_BUNDLE_URL);
const stream = new ReadableStream({
async start(controller) {
const encoder = new TextEncoder();
const send = (data: any) =>
controller.enqueue(
encoder.encode(`data: ${JSON.stringify(data)}\n\n`)
);
// Acknowledge connection
send({ type: 'ACK', entity_id: entityId });
// Attach to the running workflow's iterator
const iterator = await client.start_iterator(
entityId, 'start', []
);
for await (const envelope of iterator) {
send(envelope); // INTERNAL_UPDATE, VALUE, WAITING, etc.
}
send({ type: 'DONE' });
controller.close();
}
});
return new Response(stream, {
headers: { 'Content-Type': 'text/event-stream' }
});
}
What Makes This Production-Ready
A working demo and a production system are very different things. Here is what this architecture provides beyond the happy path.
Progress Streaming
Users see exactly what the system is doing at every moment. No spinning wheel with no context. Each pipeline stage yields typed progress events that the UI renders in real time.
Human-in-the-Loop
The AI does not act without oversight. Humans review every generated report and can provide specific feedback for revision. The system is a tool that amplifies human judgment, not one that replaces it.
Persistent Entity Graph
Every entity, every version, every reviewer decision is persisted in the entity graph. If a server restarts mid-workflow, the process resumes from where it left off. Nothing is ever lost.
Full Observability
Every LLM call, every bot request, every entity state transition is captured in telemetry. When something goes wrong -- and in production, something always does -- you can trace the exact sequence of events using ff-telemetry-read.
Architecture Overview
The final architecture uses entity delegation to break the workflow into composable stages. The review workflow entity wraps the report entity, which in turn orchestrates text extraction, AI generation, and PDF conversion as child stages.
Document Upload
|
v
ReportReviewWorkflowEntity (ReviewableEntity)
|
|-- Stores document in Working Memory
|-- Creates and delegates to ReportEntity
|
v
ReportEntity (RunnableEntity orchestrator)
|
|-- Stage 1: Extract text (doc-proc-client)
|-- Stage 2: Generate HTML (ReportGenerationEntity -> ReportGenerationBot)
|-- Stage 3: Convert to PDF (doc-proc-client)
|
v
ReviewStep
|
|-- Human approves or requests changes
|-- If rejected: re-runs with feedback
|
v
Final Result (PDF in Working Memory)
Try It Yourself
This article covered the high-level architecture and key patterns. The full 13-part tutorial walks you through every line of code, from scaffolding the project to deploying a consumer application with real-time streaming and review workflows.
The 13-Part Tutorial Series
Start the full tutorial at the FireFoundry public docs repository under the tutorials section. Each part builds on the previous one, and every step produces a deployable application you can test with CLI tools before moving on.
FireFoundry is currently in private beta. If you want to build this pipeline or explore other agent architectures, request beta access and our team will get you set up.