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:
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-listsCommit 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:
name: SPSyncon: 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:
schedules: - cron: "0 2 * * *" branches: { include: [main] } always: truepool: name: spsync-agents # a self-hosted agent pool with spsync installedsteps: - 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:
# every night at 02:000 2 * * * cd /opt/spsync && SPSYNC_DB_CONNECTION="$(cat /etc/spsync/db.env)" spsync run replication.yaml >> /var/log/spsync/run.log 2>&1Read 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:
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:
launchctl load ~/Library/LaunchAgents/io.spsync.run.plistWhere 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:
| Code | Meaning |
|---|---|
| 0 | Success |
| 1 or 3 | The run failed |
| 2 | The run completed with warnings, such as a column it skipped |
| 4 | The 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.