Last updated
SSRS and Crystal Reports expect a SQL data source, and there is no supported, fast way to point either of them at a SharePoint list directly.
The monthly compliance report is an SSRS report that has run on the first working day of every month for six years. Its source used to be a SQL table. Then the team moved its tracking into a SharePoint list, and the report lost its data. SSRS and Crystal Reports are built around SQL data sources: a connection string, a query or stored procedure, and a report definition that expects rows. Neither queries a SharePoint list well. An OData feed works as a generic web-service source, slowly, with lookup columns left as IDs. A SQL table refreshed by hand works until the person who refreshes it is on leave.
Give the report its SQL source back
SPSync replicates the lists a report depends on into SQL Server, Azure SQL, Oracle or Postgres on a schedule and keeps that copy current. The report’s dataset points at a connection string and a table or view, as it did before the move. Parameters, subreports, stored procedures and scheduled subscriptions carry on as they always have, and the data behind them is never more than one sync interval old.
- 1ConnectSign in with Microsoft, or grant per-site app permissions.
- 2DiscoverPick sites, lists and libraries, or a whole site.
- 3MapSPSync reads each list's columns and creates the tables and keys.
- 4Full loadEvery item, paged the way Microsoft recommends, into the destination.
- 5Delta syncEvery few minutes only what changed moves: inserts, updates, deletes.
- 6ReconcileWeekly by default, or on demand with the command-line tool: a full comparison to confirm SharePoint and your database match, or to catch back up after an outage.
Steps 5 and 6 repeat for as long as the replication runs. Nothing is ever written to SharePoint.
Joins and groups the report designer expects
Each list is a table with a stable primary key. A lookup arrives as a key column into the
lookup list’s table plus its display value, so the dataset shows a name without a join.
Multi-value columns are child tables, which a paginated report joins and groups like any
parent-child relationship. SPSync can also generate a reporting view per list that adds
person names, which keeps the report’s own query short. Deleted items keep their row with
sp_deleted_at set, so a report of historical totals still counts them.
| Title | Client lookup | Tags multi-choice | Owner person | Due |
|---|---|---|---|---|
| Riverside | Acme Ltd | Design; Build | Ada Lovelace | 2026-11-30 |
| Harbour | Borealis plc | Build | Alan Turing | 2027-02-14 |
then sync
- id
- title
- client_id → clients.id
- owner_id → sp_users.id
- due
- is_deleted soft delete
- sp_modified
- id
- title
- …
- project_id
- tag
One row per value: Riverside has two.
- id
- display_name
One users table shared by every list.

Repointing the data source
Pick the site and the lists the report needs, including the lookup targets it joins against. Choose the destination the report server already talks to and let SPSync create the schema. Match the sync to how often the report runs; for a monthly report, a daily sync is plenty. Then change the SSRS shared data source, or the Crystal Reports connection, to the replicated tables or a generated view.
A report server is usually always on, and the sync behind it should be too: SPSync in your Azure, or the command line scheduled next to the report server’s own jobs. One limit to plan for: SPSync replicates list and library metadata, not file bytes, so a report that embeds attachments or document content needs those handled separately.
Desktop, Azure or command line
All three editions run the same engine, write the same tables and use the same licence key, so the choice comes down to who looks after the sync.
Desktop app
For macOS and Windows. Sign in with Microsoft, tick the lists, choose a database and sync from your own machine on the schedule you set. Delegated sign-in needs no admin unless your tenant requires admin consent for apps.
In your Azure
SPSync in your Azure, a managed application for your own subscription, is deployed with you on a call while the Marketplace listing is prepared. Once deployed it is always on, so the sync keeps running with every laptop closed.
Command line
Every replication is a YAML file you can review and commit. Run it from a server, a container or a CI pipeline, and re-run it safely any time.
Set it up
Ready to run this against your own site? The setup guide takes you from install to a first replication, and the SharePoint connection guide covers signing in.
Script it
Everything you can click, you can script. The CLI, YAML config and automation below reproduce this scenario exactly. See the CLI reference.
spsync auth login --client-id 00000000-0000-0000-0000-000000000000
spsync export-config --output replication.yaml --pipeline-name ssrs_crystal_reports --prefix ssrs_crystal_reports --destination-kind mssql --connection-string-env SPSYNC_DB_CONNECTION --dataset-name ssrs_crystal_reports --client-id 00000000-0000-0000-0000-000000000000 --site-url https://contoso.sharepoint.com/sites/example --all-lists
spsync doctor replication.yaml
spsync run replication.yaml pipeline_name: ssrs_crystal_reports
prefix: ssrs_crystal_reports
auth:
mode: delegated
client_id: 00000000-0000-0000-0000-000000000000
tenant_id: common
destination:
kind: mssql
connection_string_env: SPSYNC_DB_CONNECTION
dataset_name: ssrs_crystal_reports
scopes:
- site_url: https://contoso.sharepoint.com/sites/example
mode: entire_site
reconcile_interval_days: 7
create_reporting_views: false
Cron (SQL Server nightly):
# every night at 02:00
0 2 * * * cd /opt/spsync && SPSYNC_DB_CONNECTION="$(cat /etc/spsync/db.env)" uv run spsync run replication.yaml >> /var/log/spsync/run.log 2>&1 GitHub Actions:
# .github/workflows/spsync-run.yml
name: SPSync
on:
schedule:
- cron: "0 2 * * *"
workflow_dispatch: {}
jobs:
run:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: astral-sh/setup-uv@v3
- run: uv sync
- run: uv run spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: ${{ secrets.SPSYNC_DB_CONNECTION }}
SPSYNC_LICENCE_KEY: ${{ secrets.SPSYNC_LICENCE_KEY }} Azure DevOps:
# azure-pipelines.yml
schedules:
- cron: "0 2 * * *"
branches: { include: [main] }
always: true
steps:
- script: uv sync
- script: uv run spsync run replication.yaml
env:
SPSYNC_DB_CONNECTION: $(SPSYNC_DB_CONNECTION)
SPSYNC_LICENCE_KEY: $(SPSYNC_LICENCE_KEY) Windows (PowerShell + Task Scheduler):
# Windows Task Scheduler action (see /docs/cli/automation/)
$env:SPSYNC_DB_CONNECTION = (Get-Content C:\spsync\db.env)
uv run spsync run C:\spsync\replication.yaml
schtasks /Create /SC DAILY /ST 02:00 /TN "SPSync" /TR "powershell -File C:\spsync\run.ps1" macOS (launchd):
# ~/Library/LaunchAgents/io.spsync.run.plist, then: launchctl load ~/Library/LaunchAgents/io.spsync.run.plist Secrets (licence key, connection strings) come from the environment, so never write them into the YAML. See scheduling and secrets.