RippleSync

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

  1. In Copilot Studio, go to Create → New agent → Configure and name it RippleSync Automation.
  2. Paste automation/agent-instructions.md into Instructions.
  3. Settings: turn on generative orchestration; turn off web search and general knowledge.
  4. Tools → Add a tool → Model Context Protocol:
  5. name RippleSync, address https://www.ripplesync.store/mcp/meetings;
  6. OAuth 2.0 with dynamic discovery; sign in with the dedicated account.
  7. 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).

  1. Get item (named Get_project) from the projects list with ProjectId.
  2. Parse JSON on DecisionsJson with automation/decisions.schema.json.
  3. Update item on the request: Status = Done, Error = Proposed @{length(body('Parse_JSON'))} decisions, FinishedOn = utcNow().
  4. Apply to each, named exactly Apply_to_each, over take(body('Parse_JSON'), int(outputs('Get_project')?['body/MaxDecisionsPerDay'])). Turn on concurrency, 20. Inside it:
  5. Compose (Fingerprint): toLower(replace(concat(string(triggerBody()?['number']),'|',items('Apply_to_each')?['decision'],'|',items('Apply_to_each')?['after']),' ',''))
  6. Get items (Get_existing) from the decisions list. Filter Query: Fingerprint eq '@{replace(outputs('Fingerprint'),'''','''''')}', Top 1.
  7. 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.

Save findings

Inputs: RequestId (Number), DecisionId (Number), ProjectId (Number), FindingsJson (Text), Summary (Text).

  1. Get_project. If DecisionId is above 0, Get item (Get_decision) from the decisions list.
  2. Parse JSON on FindingsJson with automation/findings-array.schema.json. Initialize variable OwnerItems as an array.
  3. Apply to each finding:
  4. 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().
  5. Append to array variable OwnerItems: {"id": <new ID>, "owner": owner_email, "link": url, "quote": quote, "fix": fix, "apply": apply_block}.
  6. If DecisionId is above 0, Update item on the decision: Status = Checked, FindingsCount = length(body('Parse_JSON')), CheckedOn = utcNow().
  7. 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.
  8. Update item on the request: Status = Done, FinishedOn = utcNow().
  9. If outputs('Get_project')?['body/NotifyOwners'] is true:
  10. Select (Select_owners) from OwnerItems, mapping to item()?['owner'], in text mode. Then Compose Owners = union(body('Select_owners'), body('Select_owners')).
  11. 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'].
  12. 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.
  13. 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

  1. Inputs: RequestId (Number), FixedIdsJson (Text), CheckedIdsJson (Text).
  2. Apply to each over json(FixedIdsJson): Update item in findings, FixStatus = Fixed, FixedOn = utcNow().
  3. Update item on the request: Status = Done, Error = @{length(json(FixedIdsJson))} of @{length(json(CheckedIdsJson))} lines fixed.

Save project

  1. Inputs: ProjectId (Number, 0 for a new project), SettingsJson (Text: the settings as a JSON object).
  2. Parse JSON.
  3. If ProjectId is 0, Create item in projects. Otherwise, Update item on ProjectId. Map each setting to its column.
  4. 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.

  1. Initialize variables:
  2. RunStart = utcNow();
  3. AdminEmail = the person to alert;
  4. FindingsListUrl = the findings list's "Open lines" view;
  5. RequestsListUrl = the requests list's "Failed runs" view.
  6. Get items from projects, with Filter Query Active eq 1.
  7. Apply to each project, named Apply_to_each_project, with concurrency 1. Inside it, p means items('Apply_to_each_project'):
  8. Compose LocalHour = int(convertTimeZone(variables('RunStart'),'UTC',p?['TimeZone'],'HH')).
  9. Compose LocalDay = convertTimeZone(variables('RunStart'),'UTC',p?['TimeZone'],'ddd').
  10. Compose LocalDate = convertTimeZone(variables('RunStart'),'UTC',p?['TimeZone'],'yyyy-MM-dd').
  11. 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.
  12. 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.
  13. 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 than addDays(utcNow(), mul(-1,int(p?['OverdueDays'])))), Filter_fixed_this_week (Fixed, with FixedOn after addDays(utcNow(),-7)) and Filter_wrong (Wrong finding).
      • Compose_card from cards/weekly-report.json, then Post card in a chat or channel to PMEmail.
  14. 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 = DocChanged and FileUrl = items('Apply_to_each_file')?['{Link}'].
    • Update item on the project: LastFileCheck = RunStart.
  15. Monitoring:
  16. 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 = Failed and Error = Stalled: no result within the time limit.
  17. 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.
  18. 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:

  1. In Copilot Studio or Power Apps, create a Solution named RippleSync Automation, and add the agent, the scheduler and the five agent flows.
  2. 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.
  3. Make sure the solution uses connection references for SharePoint, Teams and the RippleSync connection.
  4. 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.