RippleSync automation for Microsoft 365
RippleSync can run on its own inside a customer's Microsoft 365. Each project has its own settings: when to sweep, where decisions happen, who owns which document, and when it launches. Everything runs in the customer's tenant, under their Copilot Studio and Power Automate. RippleSync reads, verifies and stores nothing.
What happens automatically
| When | What RippleSync does | Who acts |
|---|---|---|
| Every day at the project's sweep time | Reads that day's project meetings, email, Teams chat and channel, and proposes each decision it finds | The PM confirms or rejects each one in a Teams card |
| A key document (such as the PRD) is edited | Compares it with the previous version and proposes any decision made by the edit | The PM confirms |
| A decision is confirmed, or the PM adds one by hand | Checks the project folder, verifies every quote, saves each line to change | Nobody: it just runs |
| After every check | Sends the PM a summary, and each owner a card with only their lines and a ready-to-paste fix | Owners apply the fix with Copilot in Word as tracked changes |
| An owner says "Done" or edits the document | Re-reads it and marks lines fixed only when the old text is really gone | Nobody |
| An owner says "Remind me in 2 days" | Reminds them, up to three times | The owner |
| An owner marks a finding wrong | Remembers it, and doesn't raise the same false flag again | Nobody |
| Days before launch (default 7) | Runs a consistency check across the whole project and reports contradictions | The PM |
| Once a week (default Monday) | Sends the PM the decision debt: lines open, overdue, fixed this week, marked wrong | The PM |
| A run fails or stalls for an hour | Alerts the PM; the next run tries again | Nobody, unless it repeats |
Repeated decisions are recognised and never proposed twice. Each sweep proposes at most the project's daily limit.
Project settings
A PM sets these by chatting with the RippleSync Automation agent ("Set up a project"), or by editing the RippleSync projects list.
| Setting | Default | What it does |
|---|---|---|
| PMEmail | the person setting it up | Gets the decision cards, summaries and weekly report |
| Active | Yes | Turn off to pause the project |
| SweepTime | 18 | Hour of the daily sweep, local time (0–23) |
| SweepDays | Mon,Tue,Wed,Thu,Fri | Days the sweep runs |
| TimeZone | India Standard Time | Windows time zone name, for example Pacific Standard Time |
| LookbackHours | 24 | How far back each sweep reads |
| MeetingTitleContains | Words in the project's meeting titles | |
| EmailSearch | Email search words | |
| ChatNameContains | Words in the project's Teams chat name | |
| ChannelLink | The project's Teams channel (⋯ → Get link to channel) | |
| DocumentsFolder | Link to the project's documents folder | |
| SiteUrl, Library, FolderPath | Where the documents live, for watching edits | |
| KeyDocuments | Links to documents whose edits count as decisions | |
| OwnerMap | Document owners: "file or folder: email", one per line | |
| NotifyOwners | Yes | Send each owner their own card |
| MaxFilesPerCheck | 12 | Files read per batch; RippleSync reads up to three batches |
| MaxDecisionsPerDay | 10 | Most decision cards per sweep |
| LaunchDate, PreLaunchDaysBefore | –, 7 | When to run the pre-launch check |
| WeeklyReportDay | Mon | Day of the weekly decision-debt report |
| OverdueDays | 5 | Days before an open line counts as overdue |
How it's built
| Piece | Type | Job |
|---|---|---|
| RippleSync projects, requests, decisions, findings | SharePoint lists | Settings, the work queue, the decision log and the ledger of lines |
| RippleSync Scheduler | Power Automate cloud flow, every hour | Starts sweeps, pre-launch checks and weekly reports at each project's local time; watches documents for edits; alerts on failures |
| RippleSync Automation | Copilot Studio agent | Does the thinking for each request in the queue; also the "set up a project" chat |
| Propose decisions, Save findings, Mark fixed, Update request, Save project | Agent flows | Teams cards, list updates and owner follow-up |
| RippleSync connection | MCP tools | collect_decision_sources, read_folder, diff_document, check_fixed, get_ripplesync_instructions, record_findings |
The scheduler only creates rows in the requests list. Every row starts the agent, which handles one request type: Sweep, DocChanged, Check, Recheck or Prelaunch. That keeps each project's timing independent and gives a full log of every run.
Before you start
- [ ] The RippleSync Microsoft 365 connection works in your tenant, and the Entra app includes Chat.Read and ChannelMessage.Read.All (both need admin consent).
- [ ] Copilot Studio is available: a trial at copilotstudio.microsoft.com, or pay-as-you-go billing set up by your admin.
- [ ] Transcription is on in the project's Teams meetings.
- [ ] A dedicated account owns the agent and flows. Triggers act with that account's permissions, so give it access only to the project sites, mailbox content and meetings it should read.
1. Create the lists
Option A, script (recommended). On a computer with PowerShell 7:
Install-Module PnP.PowerShell -Scope CurrentUser
Register-PnPEntraIDAppForInteractiveLogin -ApplicationName "PnP for RippleSync" -Tenant yourtenant.onmicrosoft.com -Interactive
./setup-lists.ps1 -SiteUrl https://yourtenant.sharepoint.com/sites/Ledgerly -ClientId <the app id it printed>
setup-lists.ps1 (in automation/lists) creates the four lists with every column, default and view, and is safe to run again.
Option B, Excel. In the site, choose New → List → From Excel for each file in automation/lists. Then change Active, NotifyOwners and Alerted to Yes/No columns, and the number and date columns to Number and Date.
2. Create the agent
- In Copilot Studio, go to Create → New agent → Configure and name it RippleSync Automation.
- Paste
automation/agent-instructions.mdinto Instructions. - Settings: turn on generative orchestration; turn off web search and general knowledge.
- Tools → Add a tool → Model Context Protocol:
- name RippleSync, address
https://www.ripplesync.store/mcp/meetings; - OAuth 2.0 with dynamic discovery; sign in with the dedicated account.
- Add these SharePoint connector tools, each pointing at the site with the lists:
| Tool name | Action | Settings |
|---|---|---|
| Get project | Get item | List: RippleSync projects; Id from the agent |
| Get decision | Get item | List: RippleSync decisions |
| Get recent decisions | Get items | List: RippleSync decisions; Filter Query ProjectId eq {ProjectId}; Order By Created desc; Top 50 |
| Get known false flags | Get items | List: RippleSync findings; Filter Query ProjectId eq {ProjectId} and FixStatus eq 'Wrong finding'; Top 30 |
3. Agent flows
Create each with Tools → Add a tool → New agent flow. Every flow starts with When an agent calls the flow, then Respond to the agent straight away, so the agent never waits for a person.
Cards. Put each card in a Compose action, pasting the JSON file as it is. The files already contain the expressions. Then pass outputs('Compose_card') to the Teams action. Compose keeps quotes and line breaks safe.
Update request
- Inputs: RequestId (Number), Status (Text), Error (Text).
- SharePoint Update item on RippleSync requests, with ID = RequestId:
- Status = Status, Error = Error;
- StartedOn =
if(equals(triggerBody()?['text'],'Running'), utcNow(), null); - FinishedOn =
if(equals(triggerBody()?['text'],'Running'), null, utcNow()).
Propose decisions
Inputs: ProjectId (Number), RequestId (Number), DecisionsJson (Text).
- Get item (named Get_project) from the projects list with ProjectId.
- Parse JSON on DecisionsJson with
automation/decisions.schema.json. - Update item on the request: Status =
Done, Error =Proposed @{length(body('Parse_JSON'))} decisions, FinishedOn =utcNow(). - Apply to each, named exactly
Apply_to_each, overtake(body('Parse_JSON'), int(outputs('Get_project')?['body/MaxDecisionsPerDay'])). Turn on concurrency, 20. Inside it: - Compose (Fingerprint):
toLower(replace(concat(string(triggerBody()?['number']),'|',items('Apply_to_each')?['decision'],'|',items('Apply_to_each')?['after']),' ','')) - Get items (Get_existing) from the decisions list. Filter Query:
Fingerprint eq '@{replace(outputs('Fingerprint'),'''','''''')}', Top 1. - Condition
empty(body('Get_existing')?['value']). If false, do nothing: it's a repeat. If true:- Create item (Create_decision) in decisions, with every field, Fingerprint and Status =
Proposed. - Compose_card from
cards/confirm-decision.json. - Post adaptive card and wait for a response: Flow bot, chat with Flow bot, Recipient =
outputs('Get_project')?['body/PMEmail'], Message =outputs('Compose_card'). - Condition
equals(body('Post_adaptive_card_and_wait_for_a_response')?['data']?['choice'],'confirm').- If yes: Update item with Status =
Confirmed, After =coalesce(body('Post_adaptive_card_and_wait_for_a_response')?['data']?['after'], items('Apply_to_each')?['after'])and PMNote =data.note. Then Create item in requests: Title =Check, ProjectId, Project, DecisionId = Create_decision's ID, Status =Queued. - If no: Update item with Status =
Not a decision.
- If yes: Update item with Status =
- Create item (Create_decision) in decisions, with every field, Fingerprint and Status =
Save findings
Inputs: RequestId (Number), DecisionId (Number), ProjectId (Number), FindingsJson (Text), Summary (Text).
- Get_project. If DecisionId is above 0, Get item (Get_decision) from the decisions list.
- Parse JSON on FindingsJson with
automation/findings-array.schema.json. Initialize variable OwnerItems as an array. - Apply to each finding:
- Create item in findings: Title =
document, DecisionId, ProjectId, Project, DocumentLink =url, OwnerEmail =owner_email, Status, Quote, Fix, Reason, ApplyBlock =apply_block, Verified =string(item verified), FixStatus =Open, CheckedOn =utcNow(). - Append to array variable OwnerItems:
{"id": <new ID>, "owner": owner_email, "link": url, "quote": quote, "fix": fix, "apply": apply_block}. - If DecisionId is above 0, Update item on the decision: Status =
Checked, FindingsCount =length(body('Parse_JSON')), CheckedOn =utcNow(). - Post message in a chat or channel: Flow bot, to PMEmail. Message: the Summary, then a link to the findings list's "Open lines" view.
- Update item on the request: Status =
Done, FinishedOn =utcNow(). - If
outputs('Get_project')?['body/NotifyOwners']is true: - Select (Select_owners) from OwnerItems, mapping to
item()?['owner'], in text mode. Then Compose Owners =union(body('Select_owners'), body('Select_owners')). - Apply to each owner, over Owners where the owner isn't empty, with concurrency 20:
- Filter array (Mine): OwnerItems where
item()?['owner']equals the current owner. - Compose Owner_lines =
join(select(body('Mine'), concat('• "', item()?['quote'], '" → ', item()?['fix'])), decodeUriComponent('%0A')). - Compose Owner_apply =
join(union(select(body('Mine'), item()?['apply']), json('[]')), decodeUriComponent('%0A%0A')). - Compose Owner_doc_link =
first(body('Mine'))?['link'].
- Filter array (Mine): OwnerItems where
- Do until the choice isn't
snooze, or 3 rounds:- Compose_card from
cards/owner-lines.json. - Post adaptive card and wait for a response to the owner.
- If the choice is
snooze: Delay 2 days.
- Compose_card from
- Switch on the choice. For each id in Mine, update the finding:
fixed: Select{id: item()?['id'], quote: item()?['quote']}from Mine, then create a request with Title =Recheck, FileUrl = Owner_doc_link and Quotes =string(body('Select')), so RippleSync confirms the old text is gone before marking anything fixed.notmine: FixStatus =Not mine, Feedback = the reason.wrong: FixStatus =Wrong finding, Feedback = the reason.
Mark fixed
- Inputs: RequestId (Number), FixedIdsJson (Text), CheckedIdsJson (Text).
- Apply to each over
json(FixedIdsJson): Update item in findings, FixStatus =Fixed, FixedOn =utcNow(). - Update item on the request: Status =
Done, Error =@{length(json(FixedIdsJson))} of @{length(json(CheckedIdsJson))} lines fixed.
Save project
- Inputs: ProjectId (Number, 0 for a new project), SettingsJson (Text: the settings as a JSON object).
- Parse JSON.
- If ProjectId is 0, Create item in projects. Otherwise, Update item on ProjectId. Map each setting to its column.
- Respond with the project's ID.
4. The scheduler (cloud flow)
Create an automated cloud flow named RippleSync Scheduler, with a Recurrence trigger every 1 hour.
- Initialize variables:
- RunStart =
utcNow(); - AdminEmail = the person to alert;
- FindingsListUrl = the findings list's "Open lines" view;
- RequestsListUrl = the requests list's "Failed runs" view.
- Get items from projects, with Filter Query
Active eq 1. - Apply to each project, named
Apply_to_each_project, with concurrency 1. Inside it,pmeansitems('Apply_to_each_project'): - Compose LocalHour =
int(convertTimeZone(variables('RunStart'),'UTC',p?['TimeZone'],'HH')). - Compose LocalDay =
convertTimeZone(variables('RunStart'),'UTC',p?['TimeZone'],'ddd'). - Compose LocalDate =
convertTimeZone(variables('RunStart'),'UTC',p?['TimeZone'],'yyyy-MM-dd'). - Sweep due:
- Condition:
and(equals(outputs('LocalHour'), int(p?['SweepTime'])), contains(p?['SweepDays'], outputs('LocalDay')), or(empty(p?['LastSweep']), less(p?['LastSweep'], addHours(variables('RunStart'),-12)))). - If true: Create item in requests (Title =
Sweep, ProjectId =p?['ID'], Project =p?['Title'], Status =Queued), then Update item on the project with LastSweep = RunStart.
- Condition:
- Pre-launch due:
- Condition:
and(not(empty(p?['LaunchDate'])), equals(formatDateTime(addDays(p?['LaunchDate'], mul(-1, int(p?['PreLaunchDaysBefore']))),'yyyy-MM-dd'), outputs('LocalDate')), equals(outputs('LocalHour'), int(p?['SweepTime']))). - If true: create a request with Title =
Prelaunch.
- Condition:
- Weekly report due:
- Condition:
and(equals(outputs('LocalDay'), p?['WeeklyReportDay']), equals(outputs('LocalHour'), int(p?['SweepTime']))). - If true:
- Get items from findings with
ProjectId eq @{p?['ID']}. - Four Filter array actions, named exactly
Filter_open(FixStatus Open),Filter_overdue(Open, with CheckedOn earlier thanaddDays(utcNow(), mul(-1,int(p?['OverdueDays'])))),Filter_fixed_this_week(Fixed, with FixedOn afteraddDays(utcNow(),-7)) andFilter_wrong(Wrong finding). - Compose_card from
cards/weekly-report.json, then Post card in a chat or channel to PMEmail.
- Get items from findings with
- Condition:
- Watch documents:
- Get files (properties only): Site =
p?['SiteUrl'], Library =p?['Library'], Limit entries to folder =p?['FolderPath'], Include nested items = Yes, Filter Query =Modified ge '@{coalesce(p?['LastFileCheck'], addHours(variables('RunStart'),-1))}'. - Get items from findings with
ProjectId eq @{p?['ID']} and FixStatus eq 'Open'. - Apply to each file:
- Filter array of those findings where
endswith(decodeUriComponent(item()?['DocumentLink']), items('Apply_to_each_file')?['{FilenameWithExtension}']). If there are any, Select{id: ID, quote: Quote}, then create a request with Title =Recheck, FileUrl =first(body('Filter_array'))?['DocumentLink']and Quotes =string(body('Select')). - If
contains(decodeUriComponent(coalesce(p?['KeyDocuments'],'')), items('Apply_to_each_file')?['{FilenameWithExtension}']), create a request with Title =DocChangedand FileUrl =items('Apply_to_each_file')?['{Link}'].
- Filter array of those findings where
- Update item on the project: LastFileCheck = RunStart.
- Get files (properties only): Site =
- Monitoring:
- Get items from requests with
(Status eq 'Running' and StartedOn lt '@{addHours(utcNow(),-1)}') or (Status eq 'Queued' and Created lt '@{addHours(utcNow(),-2)}'). Update each with Status =Failedand Error =Stalled: no result within the time limit. - Get items with
Status eq 'Failed' and Alerted eq 0. In Apply_to_each_failed:- get the project;
- Compose_card from
cards/run-failed.json; - post it to the PM;
- set Alerted = Yes.
- Error handling: put steps 2–4 in a Scope. Add Post message to AdminEmail, with Configure run after = has failed or has timed out.
5. Start the agent from the queue
In the agent, go to Triggers → Add trigger → SharePoint → When an item is created, on RippleSync requests. Keep the default payload; it's the new row. Then publish the agent, and add it to Teams and Microsoft 365 Copilot under Channels, so PMs can chat with it to set up projects.
6. Test it
| Test | Expected |
|---|---|
| Chat "Set up a project" and answer the questions | A row in projects with your settings |
| Set SweepTime to the next hour; say a decision in a transcribed meeting | One card per decision at that hour |
| Run the sweep again the same day | No repeat cards |
| Confirm a decision | Summary to the PM, one card per owner, rows in findings |
| Owner chooses "Done" without editing | Recheck keeps the lines Open |
| Owner edits the document | Within the hour, those lines become Fixed |
| Edit a sentence with a date in a key document | A decision card sourced "Document edit" |
| Set LaunchDate to 7 days from today | A pre-launch summary at sweep time |
| Set WeeklyReportDay to today | The decision-debt card at sweep time |
| Remove the connection sign-in, then run | A failed-run alert to the PM |
7. Package it for customers
Once it works in your tenant, make it installable:
- In Copilot Studio or Power Apps, create a Solution named RippleSync Automation, and add the agent, the scheduler and the five agent flows.
- Add environment variables for the lists' site address, the admin email and the two list view links. Replace the fixed values in the flows with these variables.
- Make sure the solution uses connection references for SharePoint, Teams and the RippleSync connection.
- Export it as a managed solution.
Customers then:
1. Run setup-lists.ps1 once.
2. Go to Power Apps → Solutions → Import, fill in the four variables and sign in to the three connections.
3. Publish the agent.
That's about 15 minutes instead of 2–3 hours.
Costs
Every request row starts one agent run, which uses Copilot Studio credits for the run, its tool calls and its answers. The scheduler itself uses only standard connectors. Watch usage in the Power Platform admin center during the first week. If you need to cut costs, raise SweepTime spacing, lower MaxFilesPerCheck, or remove KeyDocuments.
Troubleshooting
| What you see | Likely cause | Fix |
|---|---|---|
| No cards at sweep time | Wrong TimeZone name, or SweepDays uses other day names | Use Windows time zone names, and Mon, Tue, Wed and so on |
| A request is stuck at Queued | The agent isn't published, or its trigger is off | Publish the agent; check the trigger |
| A request failed with a sign-in error | The RippleSync connection needs signing in again | Reconnect it in the agent's tools |
| Owners don't get cards | NotifyOwners is No, or OwnerMap doesn't match file names | Fix OwnerMap ("PRD: name@company.com") |
| Lines never become Fixed | The document is outside FolderPath, or the edit didn't remove the old text | Check FolderPath; the old text must be gone |
| Teams chat or channel not read | Chat.Read or ChannelMessage.Read.All isn't consented | Grant admin consent in Entra |
Last updated 5 November 2026. Questions or feedback: use the feedback form.