Skills DirectorySearch all skills ↗

AI agent skill · by @HKUDS

Document Gen Resilient Workflow

Multi-engine document generation with cascading PDF fallbacks and robust Unicode handling

Archived 2026-10-23 · Source updated 2026-08-12 · License: MIT. Check the original repository for the latest version.

SKILL.md instructions

# Resilient Document Generation Workflow (Multi-Engine Fallback)

## When to Use

Use this skill when document generation tasks fail or when `shell_agent` returns unknown errors, especially for:
- Generating documents in multiple formats (`.docx`, `.pdf`, `.html`)
- **PDF generation fails due to missing LaTeX, encoding issues, or tool errors**
- Documents contain special characters, symbols, or non-ASCII text
- You need maximum reliability with automatic fallback options

## Core Technique

Split document generation into discrete, observable steps with **cascading fallbacks** for PDF generation:
1. **Content creation** → Use `write_file` to create source Markdown
2. **Unicode sanitization** → Use Python script for reliable character replacement
3. **Format conversion** → Try multiple PDF engines in sequence until one succeeds
4. **Verification** → Check file exists, has non-zero size, and is valid

## ⚠️ Unicode & PDF Engine Guide

Different PDF engines have different Unicode support:

| Engine | Unicode Support | Best For | Fallback Position |
|--------|----------------|----------|-------------------|
| `pdflatex` | Limited (ASCII-focused) | Simple documents | 1st (fastest) |
| `xelatex` | Full Unicode | Documents with non-ASCII | 2nd |
| `wkhtmltopdf` | Good Unicode | Web-style documents | 3rd |
| `reportlab` (Python) | Full control | Programmatic PDFs | 4th |
| `fpdf2` (Python) | Full control | Simple text PDFs | 5th (last resort) |

### Character Replacement Table

| Character | Issue | Safe Replacement |
|-----------|-------|------------------|
| `—` (em dash) | LaTeX incompatibility | `--` |
| `–` (en dash) | LaTeX incompatibility | `-` |
| `" "` (curly quotes) | Encoding errors | `" "` (straight) |
| `' '` (curly apostrophe) | Encoding errors | `'` (straight) |
| `…` (ellipsis) | May not render | `...` |
| `→` `←` `↑` `↓` | LaTeX incompatibility | `->` `<-` `^` `v` |
| `✓` `✗` | May not render | `[x]` `[ ]` |
| `★` `●` | May not render | `*` `-` |
| `©` `®` `™` | May require packages | `(c)` `(r)` `(tm)` |
| `é` `ñ` `ü` | Font-dependent | Keep for xelatex, replace for pdflatex |

## Step-by-Step Workflow

### Step 1: Create Source Content with write_file

Write your document content as Markdown to a source file:

```
write_file
path: /tmp/document_source.md
content: |
  # Document Title
  
  ## Section 1
  Content here...
  
  ## Section 2
  More content...
```

### Step 2: Sanitize Unicode with Python Script

**Create a reusable Python sanitizer** for reliable character replacement:

```
write_file
path: /tmp/sanitize_unicode.py
content: |
  #!/usr/bin/env python3
  import sys
  import re
  
  if len(sys.argv) < 2:
      print("Usage: sanitize_unicode.py <input.md> [output.md]")
      sys.exit(1)
  
  input_file = sys.argv[1]
  output_file = sys.argv[2] if len(sys.argv) > 2 else input_file.replace('.md', '_sanitized.md')
  
  replacements = {
      '—': '--',    # em dash
      '–': '-',     # en dash
      '"': '"',     # left curly quote
      '"': '"',     # right curly quote
      "'": "'",     # left curly apostrophe
      "'": "'",     # right curly apostrophe
      '…': '...',   # ellipsis
      '→': '->',    # right arrow
      '←': '<-',    # left arrow
      '↑': '^',     # up arrow
      '↓': 'v',     # down arrow
      '✓': '[x]',   # checkmark
      '✗': '[ ]',   # cross
      '★': '*',     # star
      '●': '-',     # bullet
      '©': '(c)',   # copyright
      '®': '(r)',   # registered
      '™': '(tm)',  # trademark
  }
  
  with open(input_file, 'r', encoding='utf-8') as f:
      content = f.read()
  
  for old, new in replacements.items():
      content = content.replace(old, new)
  
  with open(output_file, 'w', encoding='utf-8') as f:
      f.write(content)
  
  print(f"Sanitized: {input_file} -> {output_file}")
```

**Apply sanitization for PDF:**

```
run_shell
command: python3 /tmp/sanitize_unicode.py /tmp/document_source.md /tmp/document_source_sanitized.md
```

**Note**: Keep original for DOCX/HTML (these formats handle Unicode well).

### Step 3: Convert to Target Formats with Cascading Fallbacks

#### For DOCX (from original, no sanitization needed):

```
run_shell
command: pandoc /tmp/document_source.md -o output.docx
```

#### For PDF (try multiple engines in sequence):

**Attempt 1: pdflatex (fastest, limited Unicode)**
```
run_shell
command: pandoc /tmp/document_source_sanitized.md -o output.pdf --pdf-engine=pdflatex
```

**If pdflatex fails, Attempt 2: xelatex (full Unicode)**
```
run_shell
command: pandoc /tmp/document_source.md -o output.pdf --pdf-engine=xelatex
```

**If xelatex fails, Attempt 3: wkhtmltopdf (web-based)**
```
run_shell
command: pandoc /tmp/document_source.md -o output.pdf --pdf-engine=wkhtmltopdf
```

**If all pandoc engines fail, Attempt 4: Python reportlab**
```
write_file
path: /tmp/generate_pdf_reportlab.py
content: |
  #!/usr/bin/env python3
  from reportlab.lib.pagesizes import letter
  from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer
  from reportlab.lib.styles import getSampleStyleSheet
  import markdown
  
  input_md = '/tmp/document_source.md'
  output_pdf = 'output.pdf'
  
  with open(input_md, 'r', encoding='utf-8') as f:
      md_content = f.read()
  
  html_content = markdown.markdown(md_content)
  
  doc = SimpleDocTemplate(output_pdf, pagesize=letter)
  styles = getSampleStyleSheet()
  story = []
  
  # Simple HTML to flowables (basic implementation)
  for line in html_content.split('\n'):
      if line.strip():
          story.append(Paragraph(line, styles['Normal']))
          story.append(Spacer(1, 6))
  
  doc.build(story)
  print(f"PDF created: {output_pdf}")
```

```
run_shell
command: python3 /tmp/generate_pdf_reportlab.py
```

**If reportlab fails, Attempt 5: Python fpdf2 (simpler)**
```
write_file
path: /tmp/generate_pdf_fpdf.py
content: |
  #!/usr/bin/env python3
  from fpdf import FPDF
  
  input_md = '/tmp/document_source.md'
  output_pdf = 'output.pdf'
  
  pdf = FPDF()
  pdf.add_page()
  pdf.set_font('Helvetica', '', 12)
  
  with open(input_md, 'r', encoding='utf-8') as f:
      for line in f:
          # Simple line-by-line, escape special chars
          safe_line = line.encode('latin-1', 'replace').decode('latin-1')
          pdf.cell(0, 10, safe_line[:180], ln=True)
  
  pdf.output(output_pdf)
  print(f"PDF created: {output_pdf}")
```

```
run_shell
command: python3 /tmp/generate_pdf_fpdf.py
```

#### For HTML (from original):

```
run_shell
command: pandoc /tmp/document_source.md -o output.html
```

### Step 4: Verify Outputs with Multiple Checks

**Check 1: Files exist and have size**
```
run_shell
command: ls -lh output.docx output.pdf output.html 2>/dev/null && echo "FILES_OK" || echo "FILES_MISSING"
```

**Check 2: Validate PDF is not corrupted**
```
run_shell
command: python3 -c "import fitz; doc=fitz.open('output.pdf'); print(f'PDF_VALID: {doc.page_count} pages')" 2>/dev/null || echo "PDF_CHECK_SKIPPED"
```

**Check 3: Validate DOCX**
```
run_shell
command: python3 -c "from docx import Document; d=Document('output.docx'); print(f'DOCX_VALID: {len(d.paragraphs)} paragraphs')" 2>/dev/null || echo "DOCX_CHECK_SKIPPED"
```

**Check 4: Read back content for manual verification**
```
read_file
filetype: md
file_path: output.html
```

## Complete Example

```markdown
# Generate Negotiation Strategy Document (Resilient Workflow)

## Step 1: Write Markdown source
write_file
path: /tmp/negotiation_strategy.md
content: |
  # Negotiation Strategy
  
  ## Executive Summary
  Content with original unicode characters...
  
  ## Resolution Path
  More content...

## Step 2: Sanitize for PDF
write_file
path: /tmp/sanitize_unicode.py
content: |
  [Python sanitizer script from Step 2 above]

run_shell
command: python3 /tmp/sanitize_unicode.py /tmp/negotiation_strategy.md /tmp/negotiation_strategy_sanitized.md

## Step 3: Convert to DOCX (original, Unicode-safe format)
run_shell
command: pandoc /tmp/negotiation_strategy.md -o negotiation_strategy.docx

## Step 4: Convert to PDF (try pdflatex first)
run_shell
command: pandoc /tmp/negotiation_strategy_sanitized.md -o negotiation_strategy.pdf --pdf-engine=pdflatex

## Step 4b: If pdflatex failed, try xelatex
run_shell
command: pandoc /tmp/negotiation_strategy.md -o negotiation_strategy.pdf --pdf-engine=xelatex

## Step 4c: If xelatex failed, try wkhtmltopdf
run_shell
command: pandoc /tmp/negotiation_strategy.md -o negotiation_strategy.pdf --pdf-engine=wkhtmltopdf

## Step 5: Convert to HTML (original)
run_shell
command: pandoc /tmp/negotiation_strategy.md -o negotiation_strategy.html

## Step 6: Verify all outputs
run_shell
command: ls -lh negotiation_strategy.* && echo "ALL_FILES_CREATED"

run_shell
command: python3 -c "import fitz; d=fitz.open('negotiation_strategy.pdf'); print(f'PDF: {d.page_count} pages')"
```

## Advantages Over shell_agent

| Aspect | shell_agent | Resilient Manual Workflow |
|--------|-------------|---------------------------|
| Error visibility | Opaque, may retry silently | Each step shows explicit output |
| PDF fallback | May give up after first failure | Cascading engine attempts |
| Debugging | Hard to isolate | Clear which engine/step failed |
| Unicode control | Agent-dependent | You control sanitization |
| Recovery | Automatic but may loop | Manual intervention at known points |
| Tool requirements | Assumes pandoc works | Multiple engine options |

## Troubleshooting by Error Type

### "LaTeX not found" or "pdflatex: command not found"
- **Solution**: Try `--pdf-engine=xelatex` or `--pdf-engine=wkhtmltopdf`
- **Install LaTeX**: `apt-get install texlive-latex-recommended texlive-fonts-recommended`
- **Or fallback to Python**: Use reportlab/fpdf2 approach

### "Encoding error" or "UnicodeDecodeError"
- **Solution**: Use sanitized markdown file (Step 2)
- **Or**: Add `-f markdown+utf8` to pandoc command
- **Or**: Try xelatex engine (better Unicode support)

### "wkhtmltopdf not found"
- **Solution**: Install via `apt-get install wkhtmltopdf` or fallback to Python

### "ModuleNotFoundError: No module named 'reportlab'"
- **Solution**: `pip install reportlab` or fallback to fpdf2

### "Invalid PDF" or corrupted output
- **Diagnosis**: Run `pdftotext output.pdf -` to check if text extracts
- **Solution**: Try different PDF engine from fallback chain

### DOCX formatting issues
- **Solution**: Add `--reference-doc=template.docx` for custom styles
- **Or**: Fix markdown structure in source file

### All pandoc commands fail
- **Check**: `pandoc --version` to verify installation
- **Fallback**: Use Python-based PDF generation (reportlab/fpdf2)
- **For DOCX**: Try `python-docx` library directly

## Pre-Flight Checks (Optional but Recommended)

Before starting, verify required tools are available:

```
run_shell
command: pandoc --version && echo "PANDOC_OK" || echo "PANDOC_MISSING"
run_shell
command: pdflatex --version && echo "PDFLATEX_OK" || echo "PDFLATEX_MISSING"
run_shell
command: python3 -c "import reportlab" && echo "REPORTLAB_OK" || echo "REPORTLAB_MISSING"
```

This helps you know which fallback path will be needed.

## Related Skills

- `document-gen-unicode-safe`: Parent skill with basic Unicode guidance
- `document-gen-fallback`: Original fallback without Unicode or multi-engine support
- Use this skill when you need maximum reliability with automatic fallbacks

## When to Return to shell_agent

After manually completing this workflow successfully once for a document type, you can attempt `shell_agent` for similar tasks with reduced risk (you now know the fallback path). However, for critical documents or those with heavy Unicode content, consider always using this resilient manual workflow.

Related skills