Part of: Automation and the command line

Run a SharePoint list to database sync from CI

Schedule spsync run to copy SharePoint lists into a database from GitHub Actions, Azure DevOps, cron, Windows Task Scheduler or launchd, with secrets kept out of the YAML.

Tags: AutomationDesktopCommand line

A copy of a SharePoint list in a database is only useful while it is current. spsync run replication.yaml copies the lists named in the config into your database in one sync round: a full load the first time, then a delta that reads only what changed since the last run. The sync is one way. SPSync reads SharePoint and never writes to it, so the list stays the system of record and the database is the copy people query. If “sync” makes you think of the OneDrive sync app, what mirror, sync, copy and replicate each mean for a SharePoint list separates the four words.

The command never loops or sleeps, so something else has to decide when it runs. That can be a CI pipeline, with the config reviewed in a pull request, or the operating system’s own scheduler on a server. This page covers both, and the rule that holds for every one of them: the connection string and the licence key never go in the YAML.

What does a scheduled spsync run need from the machine?

The CLI has to be installed and on the path. It is a separate install from the desktop app, set up with you on a call (book a call), on a server, a workstation or a self-hosted build agent. Hosted CI runners do not have it, so every pipeline below runs on a self-hosted runner or agent pool.

The run also needs a sign-in it can use with nobody at the keyboard. Setup covers the CLI’s sign-in and the Graph permission each mode asks for. Finally, the destination connection string and the licence key are held as environment variables the scheduler supplies at run time, which the secrets section below explains.

Write the replication config and review it in a pull request

Run spsync export-config once, locally or from a bootstrap job. It discovers the site’s lists and writes the replication YAML:

Terminal window
spsync export-config \
--output replication.yaml \
--pipeline-name contracts \
--prefix ct \
--destination-kind postgres \
--connection-string-env SPSYNC_DB_CONNECTION \
--dataset-name contracts \
--client-id <client-id> \
--site-url https://contoso.sharepoint.com/sites/contracts \
--all-lists

Commit that file to a repository. The connection string appears in it only as a name (connection_string_env: SPSYNC_DB_CONNECTION), so the file is safe to diff and share. A change to which sites, lists or destination are replicated then goes through a pull request, and a teammate sees it before it runs anywhere.

Check the machine with spsync doctor before the first scheduled run

Run spsync doctor replication.yaml once by hand on the machine that will do the work. It checks the ODBC driver (SQL Server only), Graph connectivity and permissions, and the destination connection. Put the same command in the pipeline ahead of spsync run, so an expired permission or a bad connection string fails the job with a clear message before any data moves.

Schedule a SharePoint list sync with GitHub Actions

A scheduled workflow on a self-hosted runner, with a manual trigger for the odd re-run:

.github/workflows/spsync-run.yml
name: SPSync
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch: {}
jobs:
run:
runs-on: self-hosted # a runner with spsync installed
steps:
- uses: actions/checkout@v4
- run: spsync doctor replication.yaml
env:
SPSYNC_DB_CONNECTION: ${{ secrets.SPSYNC_DB_CONNECTION }}
- run: spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: ${{ secrets.SPSYNC_DB_CONNECTION }}
SPSYNC_LICENCE_KEY: ${{ secrets.SPSYNC_LICENCE_KEY }}

Store the two values as repository or environment secrets, never in the workflow file.

Schedule it from an Azure DevOps pipeline

Azure DevOps takes the same shape, with a scheduled trigger and a self-hosted agent pool:

azure-pipelines.yml
schedules:
- cron: "0 2 * * *"
branches: { include: [main] }
always: true
pool:
name: spsync-agents # a self-hosted agent pool with spsync installed
steps:
- script: spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: $(SPSYNC_DB_CONNECTION)
SPSYNC_LICENCE_KEY: $(SPSYNC_LICENCE_KEY)

Keep both variables in a variable group marked secret. Azure DevOps does not pass a secret variable to a script as an environment variable unless the step maps it, which is why the env block is there (see Microsoft Learn on setting secret variables).

Run the sync from cron on Linux or macOS

On a server with no CI system, a cron entry does the same job:

Terminal window
# every night at 02:00
0 2 * * * cd /opt/spsync && SPSYNC_DB_CONNECTION="$(cat /etc/spsync/db.env)" spsync run replication.yaml >> /var/log/spsync/run.log 2>&1

Read the connection string from a file only the service account can open, and keep that file out of the repository.

Schedule it with Windows Task Scheduler

Set the connection string as a machine or user environment variable first (setx SPSYNC_DB_CONNECTION "..."), then register a daily task:

Terminal window
schtasks /Create /SC DAILY /ST 02:00 /TN "SPSync" /TR "powershell -File C:\spsync\run.ps1"

Here run.ps1 holds one line, spsync run C:\spsync\replication.yaml. Run the task as the account that owns the environment variable, or it starts without one.

Schedule it with launchd on a Mac

Create ~/Library/LaunchAgents/io.spsync.run.plist with a StartCalendarInterval for the time of day, a ProgramArguments array that runs spsync run replication.yaml by its full path (launchd does not read your shell’s path), and SPSYNC_DB_CONNECTION under EnvironmentVariables. Then load it:

Terminal window
launchctl load ~/Library/LaunchAgents/io.spsync.run.plist

Where do the secrets live?

In the scheduler’s own store, every time: GitHub Actions secrets, an Azure DevOps secret variable group, a file only the service account can read for cron, or a machine environment variable on Windows. destination.connection_string_env and the licence key setting are environment variable names, resolved when the run starts. Nothing in the YAML is a secret, which is what makes it safe to commit and review.

Exit codes a scheduler should act on

The exit code tells the scheduler how the run went:

CodeMeaning
0Success
1 or 3The run failed
2The run completed with warnings, such as a column it skipped
4The run was refused because another run already holds this destination

A job that turns red on exit code 2 is often a list with a column SPSync cannot copy yet, or a date-only column with no site time zone set. Fix the column or the zone, or pass --warning-exit-code 0 so a warning run exits 0 and the warnings are still printed. Add --json and each list’s read, inserted, updated and soft-deleted counts come back in a shape a script can parse.

Only one round runs per replication and destination on a machine at a time. Two machines pointed at the same destination are not coordinated yet, so give each replication one home.

How often should the scheduled sync run?

spsync doctor’s adaptive-interval check reports the interval in effect and why. With no sync_interval_seconds in the config, SPSync estimates one from your Graph permission mode and the tenant’s request budget. Set sync_interval_seconds to pin a fixed cadence, then point the cron, Task Scheduler, launchd or pipeline schedule at the same number. A reconcile pass, which re-checks every item against SharePoint, also runs weekly by default as part of the normal schedule. Use it as the QA check that the two sides match, and to catch up after an outage on your server or in Microsoft 365.

CLI automation has the reference for each scheduler, the log file location and every exit code. To take the same config through staging, production and other sites, see Script a SharePoint list migration into a database and scriptable migrations.

Compared with

Book a call

Book a call to talk through your use case, see further walkthroughs or demos, and set up a trial in your own environment.