---
title: "Template: PDF Generator"
description: "PDF generation from structured data with platform storage upload -- render a PDF from a template and upload to platform storage"
loadWhen: "Applying the pdf-generator workflow template"
---

**Category:** Documents

**Platform Tools:** `pdf` (render), `storage` (upload)

**Credentials Required:**

- None required (pdf and storage are platform services, not integrations)

---

## What This Workflow Does

Receives structured data, renders a PDF using an HTML template, uploads the result to platform storage, and returns a signed URL for download. Suitable for invoices, reports, certificates, contracts, and any document that needs to be generated on demand and delivered as a downloadable file.

---

## Input Schema

```typescript
z.object({
  templateHtml: z.string(),          // HTML template with {variable} placeholders
  data: z.record(z.unknown()),        // Data to inject into the template
  filename: z.string(),               // Output filename (without .pdf extension)
  expiresInSeconds: z.number().optional(), // Signed URL expiry (default: 3600)
})
```

## Output Schema

```typescript
z.object({
  downloadUrl: z.string(),           // Signed URL for the generated PDF
  storageKey: z.string(),            // Storage key for future reference
  expiresAt: z.string(),             // ISO timestamp when URL expires
})
```

---

## Workflow Code Pattern

```typescript
import type { WorkflowDefinition } from '@elevasis/sdk'
import { StepType } from '@elevasis/sdk'
import { platform } from '@elevasis/sdk/worker'
import { z } from 'zod'

const inputSchema = z.object({
  templateHtml: z.string(),
  data: z.record(z.unknown()),
  filename: z.string(),
  expiresInSeconds: z.number().optional(),
})
const outputSchema = z.object({
  downloadUrl: z.string(),
  storageKey: z.string(),
  expiresAt: z.string(),
})

type Input = z.infer<typeof inputSchema>

function renderTemplate(html: string, data: Record<string, unknown>): string {
  return html.replace(/\{(\w+)\}/g, (_, key) => String(data[key] ?? ''))
}

export const pdfGenerator: WorkflowDefinition = {
  config: {
    resourceId: 'pdf-generator',
    name: 'PDF Generator',
    type: 'workflow',
    description: 'Generates a PDF from HTML template and uploads to storage',
    version: '1.0.0',
    status: 'dev',
  },
  contract: { inputSchema, outputSchema },
  steps: {
    render: {
      id: 'render',
      name: 'Render PDF',
      description: 'Render HTML template to PDF buffer',
      inputSchema,
      outputSchema: z.object({ buffer: z.string(), filename: z.string(), expiresInSeconds: z.number() }),
      handler: async (input) => {
        const { templateHtml, data, filename, expiresInSeconds } = input as Input
        const html = renderTemplate(templateHtml, data)

        const result = await platform.call({
          tool: 'pdf',
          method: 'renderToBuffer',
          params: { html },
        }) as { buffer: string } // base64-encoded buffer

        return { buffer: result.buffer, filename, expiresInSeconds: expiresInSeconds ?? 3600 }
      },
      next: { type: StepType.LINEAR, target: 'upload' },
    },
    upload: {
      id: 'upload',
      name: 'Upload PDF',
      description: 'Upload rendered PDF to platform storage and get signed URL',
      inputSchema: z.object({ buffer: z.string(), filename: z.string(), expiresInSeconds: z.number() }),
      outputSchema,
      handler: async (input, context) => {
        const { buffer, filename, expiresInSeconds } = input as { buffer: string; filename: string; expiresInSeconds: number }
        const key = `documents/${filename}-${Date.now()}.pdf`

        await platform.call({
          tool: 'storage',
          method: 'upload',
          params: {
            key,
            content: buffer,
            contentType: 'application/pdf',
          },
        })

        const urlResult = await platform.call({
          tool: 'storage',
          method: 'createSignedUrl',
          params: { key, expiresIn: expiresInSeconds },
        }) as { signedUrl: string; expiresAt: string }

        context.logger.info('PDF generated and uploaded', { key, filename })
        return { downloadUrl: urlResult.signedUrl, storageKey: key, expiresAt: urlResult.expiresAt }
      },
      next: null,
    },
  },
  entryPoint: 'render',
}
```

---

## Adaptation Notes

- **Template format:** The simple `{variable}` placeholder system handles most cases. For complex documents, suggest the user write a more complete HTML template with inline CSS.
- **Filename:** The template appends a timestamp to avoid collisions. Adapt naming to the user's convention (e.g., `invoice-{invoiceId}`).
- **Expiry:** Default is 1 hour. For document delivery workflows, increase to 24-72 hours.
- **Data structure:** Ask the user what fields their document needs before defining the `data` schema. For typed use cases (invoices, reports), define a stricter input schema.
- **Skill adaptation:** For beginners, show a complete example HTML template and explain what "template variables" are.

---

**Last Updated:** 2026-02-26
