Invoice Workflow
A multi-step workflow that processes an invoice image through structured extraction stages.
Tools
public class InvoiceTools : IAgentToolSet
{
private readonly InvoiceDbContext _db;
public InvoiceTools(InvoiceDbContext db) => _db = db;
[Tool, Description("Save the invoice header information.")]
public async Task<string> SaveInvoiceHeader(
[ToolParam("Invoice number")] string invoiceNumber,
[ToolParam("Invoice date")] string date,
[ToolParam("Vendor name")] string vendor,
[ToolParam("Currency code")] string currency,
[ToolParam("Total amount")] decimal total,
ToolContext context)
{
var tenantId = context.GetHeader("X-Tenant-Id") ?? "default";
_db.Invoices.Add(new Invoice
{
TenantId = tenantId,
InvoiceNumber = invoiceNumber,
Date = DateOnly.Parse(date),
Vendor = vendor,
Currency = currency,
Total = total,
});
await _db.SaveChangesAsync();
return $"Header saved: {invoiceNumber}";
}
[Tool, Description("Save a single invoice line item.")]
public async Task<string> SaveLineItem(
[ToolParam("Invoice number")] string invoiceNumber,
[ToolParam("Product description")] string description,
[ToolParam("Quantity")] decimal quantity,
[ToolParam("Unit price")] decimal unitPrice,
[ToolParam("Line total")] decimal lineTotal)
{
var invoice = await _db.Invoices
.FirstAsync(i => i.InvoiceNumber == invoiceNumber);
_db.LineItems.Add(new LineItem
{
InvoiceId = invoice.Id,
Description = description,
Quantity = quantity,
UnitPrice = unitPrice,
Total = lineTotal,
});
await _db.SaveChangesAsync();
return $"Line item saved: {description}";
}
}
Workflow Definition
var invoiceNumber = "";
var workflow = new Workflow("Process Invoice")
.Step(
name: "Extract header",
instruction:
"Extract the invoice number, date, vendor name, currency, and total amount. " +
"Call save_invoice_header with the extracted values.",
verify: ctx =>
{
var call = ctx.ToolInvocations.FirstOrDefault(t => t.ToolName == "save_invoice_header");
if (call is not null)
invoiceNumber = ExtractArgument(call.Arguments, "invoiceNumber");
return Task.FromResult(call is not null);
})
.Step(
name: "Extract line items",
instruction:
"Extract every line item from the invoice. " +
"Call save_line_item for each one. Do not skip any items.",
verify: async ctx =>
{
await using var db = await dbFactory.CreateDbContextAsync();
var invoice = await db.Invoices
.Include(i => i.LineItems)
.FirstOrDefaultAsync(i => i.InvoiceNumber == invoiceNumber);
return invoice?.LineItems.Count > 0;
})
.Step(
name: "Confirm totals",
instruction:
"Sum the line item totals and confirm they match the invoice total. " +
"State explicitly whether they match or there is a discrepancy.",
verify: ctx =>
Task.FromResult(ctx.ResponseText.Contains("match", StringComparison.OrdinalIgnoreCase)
|| ctx.ResponseText.Contains("discrepancy", StringComparison.OrdinalIgnoreCase)));
Running the Workflow
var lm = new OpenAIBackend(new LMConfig
{
Endpoint = "http://localhost:1234",
ModelName = "your-vision-model",
});
var agent = new Agent(lm, new AgentOptions
{
SystemPrompt = "You are an invoice processing assistant.",
OnEvent = e =>
{
if (e.Kind == AgentEventKind.TextDelta) Console.Write(e.Text);
if (e.Kind == AgentEventKind.StepCompleted) Console.WriteLine($"\n✓ Step complete");
},
});
var result = await agent.RunWorkflowAsync(
workflow,
input: "Process this invoice image.",
images: ["/tmp/invoice.jpg"],
mcpServerUrl: "http://localhost:5100/mcp",
maxRounds: 15);
if (result.Completed)
Console.WriteLine($"\n✅ All {result.Steps.Count} steps completed.");
else
{
var failed = result.Steps.Where(s => s.Status == WorkflowStepStatus.Pending);
Console.WriteLine($"\n❌ Failed steps: {string.Join(", ", failed.Select(s => s.Name))}");
}
What it demonstrates
- Multi-step workflow with ordered, verified execution
- Async database guardrails in verify callbacks
ToolContextfor tenant isolation- Combining vision (image input) with tool calls
- Tracking state between workflow steps using closures