Use a per-user launchd LaunchAgent to run a browser automation script on a calendar schedule. The script opens the website and saves a viewport or full-page image. This captures the rendered web page, not everything visible on the Mac desktop; desktop, app, or window capture is a separate macOS screen-capture task.
Choose what you want to capture
| Approach | What it captures | Best fit | Permissions and output |
|---|---|---|---|
| Browser page screenshot | A page rendered in a browser, either the current viewport or the full scrollable page. | Recurring snapshots of a website, including a full-page capture. | Use browser automation such as Playwright. The browser can run independently of what is currently displayed on the desktop. |
| macOS screen-content capture | Screen content such as displays, apps, and windows. | A capture of what is shown on the desktop rather than a specific website page. | ScreenCaptureKit is Apple’s screen-content capture framework. Apple’s sample documents a first-use Screen Recording prompt. It is not a substitute for a browser page screenshot when you need the full scrollable page. Apple ScreenCaptureKit |
The steps below use Playwright for a website page image. Apple’s archived Daemons and Services Programming Guide recommends launchd for timed jobs, with a separate property list describing each job. Its detailed scheduling examples are archived documentation; check behavior on the macOS release you use rather than assuming every historical detail has been verified on all current releases. Apple: Creating Launch Daemons and Agents
Set up the browser screenshot script
Install Node.js and Playwright in a stable project directory under your user account. The example saves a full-page PNG to a directory in your home folder. Run these commands in Terminal from the project directory:
npm init -ynpm install playwrightnpx playwright install chromium- Create
capture.mjswith the code below, replacing the example URL and destination if needed.
import { chromium } from 'playwright';
const browser = await chromium.launch({ headless: true });
try {
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
await page.goto('https://example.com', { waitUntil: 'networkidle', timeout: 60000 });
await page.screenshot({ path: `${process.env.HOME}/website-shots/example.png`, fullPage: true });
} finally {
await browser.close();
}
Create the output directory before the first run with mkdir -p "$HOME/website-shots". Verify the script manually with node "$HOME/path-to-project/capture.mjs" (use the actual project path). A successful run should create example.png at the specified destination.
#1 Best Overall
Viewport versus full page
Set fullPage: false to capture only the viewport, or fullPage: true for the page’s full scrollable length. Playwright’s page screenshot API writes to the path you provide. Very long pages can create large image files; choose viewport dimensions and output format to suit your downstream use. Playwright: Screenshots
Wait strategy and dynamic sites
The example waits for networkidle before capture and allows up to 60 seconds for navigation. Some sites keep background requests active, so network-idle waiting may never settle. For those pages, use a deliberate fixed delay or wait for a page-specific selector instead of relying on network idle; ensure the selected element indicates that the content you need has rendered. Authentication and browser state are not automatically guaranteed in a background run: use only a supported, intentional login approach and confirm the job can access required credentials and state.
Create a per-user LaunchAgent
A LaunchAgent runs on behalf of the logged-in user, making it the natural context when the script needs that user’s files or browser-related state. A LaunchDaemon runs in a system context and may run before a user logs in; do not choose one simply because the task is scheduled. A background process might not have the browser state or credentials you expect. Apple describes agents and daemons in its launchd documentation. Apple: Creating Launch Daemons and Agents
Rank #2
- Intuitive interface of a conventional FTP client
- Easy and Reliable FTP Site Maintenance.
- FTP Automation and Synchronization
- Create the LaunchAgents folder if needed:
mkdir -p "$HOME/Library/LaunchAgents". - Save the property list below as
$HOME/Library/LaunchAgents/com.example.website-screenshot.plist. Replace both occurrences of/Users/YOURNAMEwith your actual home-directory path, and set the time fields to your desired local schedule. - Load the plist for your user with
launchctl bootstrap gui/$(id -u) "$HOME/Library/LaunchAgents/com.example.website-screenshot.plist". - Check the job’s status with
launchctl print gui/$(id -u)/com.example.website-screenshot. Then verify a run and its log output before relying on the schedule.
<?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.website-screenshot</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/node</string>
<string>/Users/YOURNAME/path-to-project/capture.mjs</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key>
<integer>9</integer>
<key>Minute</key>
<integer>0</integer>
</dict>
<key>StandardOutPath</key>
<string>/Users/YOURNAME/website-shots/launchd.log</string>
<key>StandardErrorPath</key>
<string>/Users/YOURNAME/website-shots/launchd-error.log</string>
</dict>
</plist>
This example schedules a daily run at 9:00 a.m. local time. In StartCalendarInterval, the supplied time fields define when the job runs; omitted fields behave as wildcards. For example, adding a Weekday field limits the schedule to selected weekdays. Use a unique Label for each job. Apple’s detailed plist and scheduling guidance is in its archived guide. Apple: Creating Launch Daemons and Agents
Use the correct executable path
The Node.js path in the sample is common for some Apple Silicon Homebrew installations, not universal. Find the executable with which node in the same environment where Playwright works, and put that absolute path in ProgramArguments. Do not depend on shell startup files or an interactive terminal’s PATH; launchd jobs should use explicit executable and script paths.
Change or unload the schedule
After editing a loaded plist, boot it out and bootstrap it again so the job uses the updated configuration:
Rank #3
launchctl bootout gui/$(id -u) "$HOME/Library/LaunchAgents/com.example.website-screenshot.plist"
launchctl bootstrap gui/$(id -u) "$HOME/Library/LaunchAgents/com.example.website-screenshot.plist"
To stop managing the job, run the bootout command. Keep the plist in ~/Library/LaunchAgents for a user-level job; app developers managing their own helpers may use Apple’s SMAppService, available for registering and controlling LaunchAgents and LaunchDaemons beginning with macOS 13. That API is not required for a user manually creating a plist. Apple: SMAppService
Account for sleep, shutdown, and Safari
What happens if the Mac sleeps or is off?
Apple’s archived Daemons and Services Programming Guide, “Scheduling Timed Jobs,” says: “If you schedule a launchd job by setting the StartCalendarInterval key and the computer is asleep when the job should have run, your job will run when the computer wakes up.” This describes a missed run during sleep, not an exact-time capture. A job missed because the Mac is powered off waits until the next scheduled time. Apple: Scheduling Timed Jobs
Free tools Windows power users keep installed
One-click scans. No signup required.
If you specifically need Safari remote automation
Safari WebDriver is one option, not a requirement for every browser workflow. Apple’s WebDriver instructions say to turn on Allow remote automation in Safari’s Developer Settings, or run safaridriver --enable in Terminal. Follow Apple’s current instructions for the Safari and macOS versions in use. Apple: Testing with WebDriver in Safari
Rank #4
Keep recurring screenshots comparable
If the goal is to spot visual changes, keep the browser version, operating-system environment, viewport, and capture mode stable where practical. Playwright notes that screenshots can vary with the OS version, settings, hardware, power source, and headless mode. Page content can also change independently of your code, so a difference between two captures does not by itself identify its cause. Playwright: Visual comparisons
- Use a fixed viewport and the same full-page or viewport setting for each run.
- Keep the browser and host environment consistent where possible.
- Choose a wait condition tied to the content you need, rather than assuming every page loads identically.
- Use timestamped filenames if each scheduled run should be retained; otherwise, a fixed filename will be overwritten by the next successful capture.
Troubleshoot common failures
| Symptom | Likely cause | What to check |
|---|---|---|
| No screenshot appears | The job did not load, the script path is wrong, or the destination directory is missing or unwritable. | Run the Node command manually, confirm the absolute paths in ProgramArguments, create the output folder, then inspect the configured standard output and error logs. |
| The job works in Terminal but not on schedule | The job is using a different environment, executable path, permissions, or user context. | Use absolute paths, confirm the LaunchAgent is bootstrapped for the logged-in user, and verify any required browser state or credentials are available to that job. |
| Navigation times out | The site is slow, unreachable, or keeps network activity open so the chosen wait condition does not settle. | Check the error log and URL, then replace networkidle with a suitable selector or fixed wait where appropriate; adjust the timeout only when the page genuinely needs longer. |
| Image is cut off or unexpectedly large | The capture mode does not match the desired output, or the page is exceptionally long. | Set fullPage according to whether you need the viewport or full scrollable page, and review the viewport and destination storage. |
| Run appears at the wrong time or is missed | The plist time fields are incorrect, the Mac was asleep, or it was powered off. | Check StartCalendarInterval and the Mac’s local time; distinguish a delayed wake-triggered run from a powered-off missed occurrence. |
Or skip the browser setup:
ScreenshotNeo provides a one-request website screenshot API; the code below saves a WebP capture. See the ScreenshotNeo API documentation for request options.
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
ScreenshotNeo accepts cookie and consent banners before capture and removes more than 60 known consent platforms, newsletter popups, and chat widgets; each step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and the response identifies the page verdict and billing status in headers. Its MCP server offers take_screenshot, get_page_info, and capture_pdf tools for AI agents using Claude, Cursor, or another MCP client. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. ScreenshotNeo
Sign up free for 1,000 screenshots a month with no card.
Best Value
Frequently Asked Questions
Can a LaunchAgent run while no user is logged in?
A LaunchAgent runs on behalf of a logged-in user. A LaunchDaemon runs in a system context and may run before a user logs in, but that does not guarantee access to a user’s browser state or credentials.
Does a full-page screenshot capture the macOS desktop?
No. Playwright’s full-page option captures the website’s scrollable page. ScreenCaptureKit captures screen content such as displays, apps, and windows.
Quick Recap
Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

