Figment.so
Mac hostingWebsite hostingGuidesHow to usePricing

Create Scheduled PDF Reports on a Hosted Mac

Scheduled PDF reports fail in two predictable ways: the HTML renders before JavaScript data arrives, or the file exists but is empty, truncated, or an HTML error page saved with a .pdf suffix. This guide builds a small, reliable pipeline on a hosted Mac that catches both before the report reaches a recipient.

Pick a Figment plan for this job

For almost every HTML-to-PDF reporting workload, the right starting point is Figment's $29/month virtual Intel macOS environment (2 vCPU, 8 GiB RAM, 80 GiB disk). Rendering a dashboard to a PDF is CPU- and memory-bound during layout; it does not need Apple silicon, GPU acceleration, or audio. You access the environment through the browser console and run commands from its desktop terminal.

Consider moving up when the job outgrows the VM:

  • Heavy JavaScript dashboards that benefit from a faster physical core → i3 Mac at $49/month (request-based while supply is confirmed).
  • You want CI setup, backups, monitoring, and 30 minutes of monthly support bundled on one M-series Mac → Managed Build Mac at $249/month.
  • Multiple concurrent renders, ARM-only tooling, or large local datasets before rendering → M-series Mac from $95/month; the exact chip, RAM, and disk are confirmed in the quote.

All plans are billed monthly in advance. A two-week project on the $29 VM is still one month of billing — there is no published daily rate. See Mac Hosting for current plan details.

Tools available on macOS

ToolInstallBest for
wkhtmltopdfbrew install wkhtmltopdfURL or HTML file → PDF with JavaScript support
macOS Shortcuts (shortcuts run)Built-inNo-code multi-step flows; see Apple's Shortcuts CLI guide
Headless Chrome (--print-to-pdf)brew install --cask google-chromeModern CSS, strict JS timing control
cupsfilterBuilt-inConverting already-printable formats

wkhtmltopdf is the most predictable choice for scheduled jobs: headless, URL or file input, configurable JS delay, deterministic output. The upstream project is archived, so pin a known-good Homebrew version and check the wkhtmltopdf option reference — the default --javascript-delay is 200 ms, which is almost always too short for data-driven pages.

Report script with completeness checks

Save this to ~/bin/generate-report.sh and chmod +x it. It is the artifact for this page: every PDF is checked four ways before it is treated as a valid report.

#!/usr/bin/env bash
set -euo pipefail

REPORT_URL="${1:?Usage: generate-report.sh <url> <output.pdf>}"
OUTPUT_PDF="${2:?}"
LOG="${HOME}/Library/Logs/pdf-reports.log"
MIN_BYTES=10000   # Tune to the smallest valid report you have ever produced.

ts() { date -u +%FT%TZ; }

# 1. Render
if ! wkhtmltopdf \
      --page-size A4 \
      --javascript-delay 3000 \
      --title "Scheduled Report $(date +%F)" \
      --footer-center "Page [page] of [topage]" \
      --footer-font-size 9 \
      "$REPORT_URL" "$OUTPUT_PDF" >> "$LOG" 2>&1; then
  echo "$(ts) FAIL render" >> "$LOG"; exit 1
fi

# 2. File present
[[ -f "$OUTPUT_PDF" ]] || { echo "$(ts) FAIL missing" >> "$LOG"; exit 1; }

# 3. Minimum size
BYTES=$(wc -c < "$OUTPUT_PDF" | tr -d ' ')
(( BYTES >= MIN_BYTES )) || { echo "$(ts) FAIL small ${BYTES}" >> "$LOG"; exit 1; }

# 4. PDF magic header (catches HTML error pages saved as .pdf)
[[ "$(head -c 5 "$OUTPUT_PDF")" == "%PDF-" ]] \
  || { echo "$(ts) FAIL header" >> "$LOG"; exit 1; }

echo "$(ts) OK ${OUTPUT_PDF} ${BYTES}" >> "$LOG"
CheckWhat it catches
Exit codeNetwork failure, renderer crash
File presentSilent failure before any write
Minimum bytesEmpty page, whitespace-only output, truncation
%PDF- headerLogin page, 403/404, or proxy error written as .pdf

This is an unexecuted example. Run it once by hand against your real URL before scheduling it.

Tune the JavaScript delay for your data source

Open the report URL in a browser, open DevTools → Network, reload, and note when the last XHR or fetch completes. Set --javascript-delay to that time plus a 500 ms buffer. Confirm by running the script manually and opening the PDF — if a chart is missing, increase the delay. Dashboards backed by APIs usually settle between 2,000 and 4,000 ms.

Schedule with launchd

Place this at ~/Library/LaunchAgents/com.example.pdf-report.plist and load it with launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.pdf-report.plist.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>com.example.pdf-report</string>
  <key>ProgramArguments</key>
  <array>
    <string>/bin/bash</string>
    <string>/Users/agent/bin/generate-report.sh</string>
    <string>https://your-dashboard.example.com/weekly</string>
    <string>/Users/agent/reports/weekly-$(date +%F).pdf</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Weekday</key><integer>1</integer>
    <key>Hour</key><integer>7</integer>
    <key>Minute</key><integer>0</integer>
  </dict>
  <key>StandardOutPath</key><string>/Users/agent/Library/Logs/pdf-reports.log</string>
  <key>StandardErrorPath</key><string>/Users/agent/Library/Logs/pdf-reports-error.log</string>
</dict>
</plist>

launchd expands StartCalendarInterval as calendar wildcards — missing keys mean "any". For the exact semantics of Weekday, Day, and catch-up runs when the machine is asleep, read man launchd.plist on your Mac; the authoritative description lives there rather than in the archived HTML. Shell variables like $(date +%F) are not expanded inside the plist; compute the filename inside the script instead.

Cost and operating worksheet

A concrete monthly check for this workload on the $29 VM:

  • Base: $29/month, billed in advance, no published daily rate.
  • Storage: 80 GiB on-disk. One PDF at ~200 KB × 30/day × 90 days retention ≈ 540 MB — well under the included disk.
  • Egress to S3 or email: a 200 KB file × 30/day × 30 days ≈ 180 MB/month — negligible, but tax and any fees are whatever the signed order lists.
  • Upgrade trigger: if top during a render shows sustained >80% CPU and the job regularly misses its window, price the i3 or the Managed Build Mac against the time you spend babysitting the current one.

Homebrew and wkhtmltopdf are free. Any charting library, data API, or email provider you call has its own licence and quota — confirm those separately.

Recovery checks after the first scheduled run

  • tail -n 50 ~/Library/Logs/pdf-reports.log — expect OK lines with stable byte counts.
  • If bytes drift down, the source page likely changed; adjust MIN_BYTES only after you understand why.
  • If the job requires VPN or an authenticated cookie, make the script fail fast when the cookie jar is missing rather than rendering a login page.
  • If launchctl list | grep pdf-report shows a non-zero exit, inspect pdf-reports-error.log before re-loading the agent.

Start on Figment

Open Mac Hosting, choose the $29 virtual Intel macOS plan (or request a quote for i3, M-series, or Managed Build), sign in to the browser console, install wkhtmltopdf with Homebrew, drop in the script and plist above, and run one manual render end-to-end before enabling the schedule.

Start your Mac →

Related guides:


Subscribe for Figma design and website publishing guides.


Figment.so

Contact

Twitter

Website pricing

Compute index

About

Privacy

Terms