Indhold
Ét flow til ændringer på mange SharePoint-lister med webhooks
Et flow opretter og fornyer webhooks på alle lister med et bestemt navn på tværs af websteder, og et andet modtager ændringerne. Begge flows trin for trin.
SharePoint-triggeren i Power Automate lytter på én liste. Skal ét flow reagere på ændringer i mange lister — her alle lister, hvis navn begynder med contracts-, spredt over flere websteder — er løsningen SharePoints webhooks. Et webhook-abonnement beder SharePoint om at sende besked til en bestemt adresse, hver gang der sker noget på en liste:
- nyt eller ændret element på liste A → besked til flowets adresse
- nyt eller ændret element på liste B → besked til samme adresse
Løsningen kan tage et vilkårligt antal lister og websteder. Den består af to flows og to lister:
- Abonnementsflowet opretter og fornyer abonnementerne.
- Modtagerflowet får beskederne og gør arbejdet.
- En liste over websteder, som abonnementsflowet går igennem. Den har kolonnerne
TitleogURL. - En liste med tidsstempler, én række pr. overvåget liste, med kolonnerne
Title,listGUIDogLastModificationDateTime.

Beskeden fra SharePoint siger kun, at noget er ændret på en liste, ikke hvad eller hvilket element. Derfor tidsstemplerne: modtagerflowet henter alt, der er ændret siden sidste gang. Et tidsstempel finder kun elementer, der stadig findes. Skal slettede elementer med, er GetChanges med et ændringstoken Microsofts løsning. Den står i afsnittet Med GetChanges i stedet for tidsstempler.
Modtagerflowet starter med When a HTTP request is received, som er en premiumtrigger. Er der alligevel brug for premium, er Azure Logic Apps et godt alternativ: der betales pr. kørsel, og styringen af kørsler er bedre. Opbygningen er den samme, og udtrykkene er de samme.
Modtagerflowet#
Det bygges først. Dets adresse findes først, når det er gemt, og abonnementsflowet skal bruge den. SharePoint afprøver også adressen, når et abonnement oprettes, så modtagerflowet skal kunne svare, før det første abonnement kan laves.
When a HTTP request is received. Adressen (HTTP URL) dannes, når flowet gemmes første gang. Det er den adresse, abonnementsflowet skal have som notificationURL. Skemaet til body genereres ud fra en rigtig besked, fx fra en første kørsel.
Lige efter triggeren initialiseres variablerne header_nometa (samme værdi som i abonnementsflowet), ListGUID, listSiteURL og lastModifiedTime. Initialize variable kan ikke stå inde i en betingelse, så de sættes senere med Set variable.
Condition — er det en afprøvning? SharePoint afprøver adressen med en tom body og en validationtoken i adressen:
int(triggerOutputs()['headers']?['Content-Length']) er lig med 0
Ja-grenen svarer med tokenet og stopper:
- Response, statuskode
200, bodytriggerOutputs()['queries']?['validationtoken'] - Terminate, status Succeeded
Nej-grenen svarer straks med en Response, statuskode 200 og tom body. Får SharePoint ikke svar inden for fem sekunder, eller et svar uden for 200–299, prøver det igen op til fem gange med fem minutters mellemrum og dropper så beskeden. Næste ændring giver en ny besked. Microsoft beskriver ikke, at abonnementet slås fra efter fejl.
Resten står i nej-grenen, efter svaret.

Set variable ListGUID — listens id står i beskeden:
first(triggerBody()['value'])?['resource']
Set variable listSiteURL — beskeden har webstedets relative adresse:
concat('https://<tenant>.sharepoint.com',first(triggerBody()['value'])?['siteUrl'])
Send an HTTP request to SharePoint (Get lastModified time for list) mod webstedet med tidsstempellisten, metode GET, headers @variables('header_nometa'):
_api/web/lists(guid'<tidsstempellistens-id>')/items()?$filter=listGUID eq '@{variables('ListGUID')}'
Set variable lastModifiedTime:
first(body('Get_lastModified_time_for_list')['value'])?['LastModificationDateTime']
Update item på tidsstempellisten, så den er klar til næste besked:
- Id:
first(body('Get_lastModified_time_for_list')?['value'])?['Id'] - Title:
first(body('Get_lastModified_time_for_list')?['value'])?['Title'] - LastModificationDateTime:
utcNow()
Get items henter det, der er ændret siden sidst:
- Site Address:
@variables('listSiteURL') - List Name:
@variables('ListGUID') - Filter Query:
Modified ge '@{variables('lastModifiedTime')}'
Derefter kommer det, flowet egentlig skal, for hvert element.
Abonnementsflowet#
Recurrence, én gang om måneden.
Fem variabler:
| Navn | Type | Værdi |
|---|---|---|
header_nometa | Object | { "Accept": "application/json; odata=nometadata" } |
flowName | String | workflow()?['name'] |
notificationURL | String | modtagerflowets HTTP URL |
expirationDateTime | String | addDays(utcNow(),60) |
SubscriptionID | String | tom |
flowName er flowets id. Det gemmes som clientState på abonnementet, så flowet senere kan kende sine egne abonnementer fra andres.
Get items (Get Sites) på listen over websteder.
Apply to each (For each item in list) over body('Get_Sites')?['value']:
- Send an HTTP request to SharePoint (
Get contract lists), siteitems('For_each_item_in_list')?['URL'], metodeGET. Filteret bestemmer, hvilke lister der overvåges:
_api/web/lists?$filter=BaseType eq 0 and Hidden eq false and startswith(Title, 'contracts-')
Select (
Select IDs from contract lists) frabody('Get_contract_lists')['d']['results'], medId→item()?['Id'].Select (
Select Making batch requests) frabody('Select_IDs_from_contract_lists'). Mappingen står i teksttilstand og laver hvert id om til listens abonnementsadresse:
"_api/web/lists(guid'@{item()?['Id']}')/Subscriptions"
- Apply to each (
For each subscription URI) overbody('Select_Making_batch_requests'):
- Send an HTTP request to SharePoint (
Get subscription details), siteitems('For_each_item_in_list')?['URL'], metodeGET, URIitems('For_each_subscription_URI'), headersvariables('header_nometa'). - Filter array fra
body('Get_subscription_details')['value']medequals(item()?['clientState'], variables('flowName')). En liste kan have abonnementer fra andre løsninger, og kun flowets egne skal røres. - Set variable
SubscriptionIDtilfirst(body('Filter_array'))?['Id']. Flowet laver højst ét abonnement pr. liste, så det første er det rigtige. - Compose (
Get resource id) trækker listens id ud af adressen:
replace(replace(item(),'_api/web/lists(guid''',''),''')/Subscriptions','')
- Condition
length(body('Filter_array'))er større end0. Findes abonnementet, fornyes det. Ellers oprettes det. Er et abonnement blevet slettet, bliver det altså oprettet igen ved næste kørsel.

Ja-grenen fornyer med Send an HTTP request to SharePoint, metode PATCH, headers variables('header_nometa'):
concat(items('For_each_subscription_URI'),'(''',variables('SubscriptionID'),''')')
Nej-grenen opretter med metode POST og URI @{items('For_each_subscription_URI')}. Begge grene har samme body:
{
"resource": "@{outputs('Get_resource_id')}",
"notificationUrl": "@{variables('notificationURL')}",
"expirationDateTime": "@{variables('expirationDateTime')}",
"clientState": "@{variables('flowName')}"
}
Med GetChanges i stedet for tidsstempler#
Tidsstemplerne og Modified ge finder kun elementer, der stadig findes. Et slettet element er væk fra Get items og kan kun ses i papirkurven eller i listens ændringslog. Microsoft skriver om beskeden: “The notification doesn’t include any information about the changes that triggered it.” Microsofts anbefaling er at spørge listen med GetChanges og et ændringstoken, der gemmes til næste gang. Svaret har nye, ændrede og slettede elementer siden tokenet, også dem fra beskeder, der gik tabt.
Ændringen i modtagerflowet er lille. Tidsstempellisten får en tekstkolonne ChangeToken i stedet for LastModificationDateTime, variablen lastModifiedTime bliver til changeToken, og Get items bliver til et kald til GetChanges. Rækken pr. liste læses stadig med Get lastModified time for list.
Tokenet. Et ændringstoken for en liste har formen:
1;3;<liste-id>;<tidspunkt i ticks, UTC>;<ændringsnummer>
3 betyder, at tokenet gælder en liste. Microsofts eget eksempel laver et starttoken ud fra et tidspunkt og -1 som ændringsnummer. I Power Automate giver ticks() det samme tal som .NET, så starttokenet til en ny række er:
concat('1;3;', variables('ListGUID'), ';', string(ticks(addMinutes(utcNow(),-5))), ';-1')
Listens aktuelle token er egenskaben CurrentChangeToken i CSOM. Som _api/web/lists(guid'<liste-id>')?$select=CurrentChangeToken har jeg ikke set det dokumenteret for REST.
Set variable changeToken:
first(body('Get_lastModified_time_for_list')['value'])?['ChangeToken']
Send an HTTP request to SharePoint (GetChanges) mod listSiteURL, metode POST:
_api/web/lists(guid'@{variables('ListGUID')}')/GetChanges
Headers:
Accept: application/json;odata=nometadata
Content-Type: application/json;odata=verbose
Body:
1{
2 "query": {
3 "__metadata": { "type": "SP.ChangeQuery" },
4 "Item": true,
5 "Add": true,
6 "Update": true,
7 "DeleteObject": true,
8 "ChangeTokenStart": {
9 "__metadata": { "type": "SP.ChangeToken" },
10 "StringValue": "@{variables('changeToken')}"
11 }
12 }
13}
Item begrænser svaret til elementer. Add, Update og DeleteObject vælger, hvilke slags ændringer der kommer med. Microsoft viser GetChanges med query og SP.ChangeQuery og viser ChangeTokenStart i CSOM. Om kaldet virker samlet som her, er uvist.
Svaret. value er en liste af ændringer. Hver har ItemId, ChangeType og sit eget ChangeToken:
ChangeType | Betyder |
|---|---|
| 1 | Tilføjet |
| 2 | Ændret |
| 3 | Slettet |
| 7 | Gendannet |
Et element, der er ændret tre gange, står der tre gange. Select i teksttilstand med item()?['ItemId'] og derefter union() af resultatet med sig selv giver hvert id én gang. Tilføjede og ændrede hentes med Get item. Slettede kan ikke hentes, kun id’et er tilbage.
Update item på tidsstempellisten kommer nu efter kaldet, for det nye token er det sidste i svaret. Er value tom, beholdes det gamle:
if(empty(body('GetChanges')?['value']), variables('changeToken'), last(body('GetChanges')?['value'])?['ChangeToken']?['StringValue'])
Microsofts eksempelkode sætter FetchLimit på forespørgslen til 1000 og henter igen fra det sidste token, hvis der er flere.
Faldgruber#
- Select mister sine anførselstegn.
Select Making batch requestser ofte grunden til, at flowet ikke vil gemmes: designeren fjerner"i begyndelsen og slutningen af mappingen. Sæt dem igen, og gem. - Modtagerflowet skal kunne svare, før abonnementsflowet køres. Ellers fejler oprettelsen, fordi SharePoint ikke får tokenet tilbage. SharePoint venter højst fem sekunder på svaret, både ved afprøvningen og ved hver besked. Derfor svarer modtagerflowet, før det gør noget andet.
- Abonnementer udløber. Den længste levetid er 180 dage. Her sættes den til 60 dage, og flowet kører månedligt, så der er god margen. Fornyelsen skal ske efter en plan, ikke når der kommer en besked, for en liste uden ændringer i et halvt år sender ingen.
- Hvem må starte modtagerflowet. Triggeren har i dag indstillingen Who can trigger the flow, og nye flows står som standard til brugere i organisationen. SharePoint sender ingen Entra-token med, så den skal stå til Anyone. Adressen er så selve nøglen og skal behandles som en secret.
clientStatekan tjekkes i modtagerflowet som et ekstra værn. - Flowadresser på
logic.azure.comholdt op med at virke den 30. november 2025. Flows med HTTP-trigger fik en ny adresse. Er løsningen bygget før, skalnotificationURLhave den nye adresse, og abonnementsflowet skal køres, så PATCH flytter abonnementerne. - Variabler i en løkke kræver sekventiel kørsel.
SubscriptionIDsættes inde i en Apply to each. Løkken skal have samtidighed slået fra (grad 1), ellers overskriver gennemløbene hinandens værdi. - Én besked kan dække flere ændringer.
valuei beskeden er et array, ogfirst()tager kun det første. Det går, fordi flowet alligevel henter alt ændret siden sidst, men beskeder, der kommer samtidig, kan læse det samme tidsstempel. Samtidighed slået fra på triggeren gør modtagerflowet sekventielt. - Tidsstempellisten skal have en række pr. liste. Findes rækken ikke, er
first()tom, og Update item fejler. Rækken oprettes, når abonnementet oprettes, eller i hånden. - Løkkernes navne indgår i udtrykkene. Hedder løkken noget andet end
For_each_subscription_URI, skal udtrykkene rettes til. - Afprøvningen kommer som POST ifølge én side hos Microsoft og som GET ifølge en anden. Modtageren bør kunne svare på begge med
validationtokensom tekst og status 200. At en Response som første handling altid når frem inden for fem sekunder, lover Microsoft ikke. - Tabte beskeder koster ikke ændringer med GetChanges. Næste gang
GetChangeskøres med det gemte token, kommer også de ændringer med, som den tabte besked handlede om. Med tidsstempler gælder det samme for nye og ændrede elementer, men ikke for slettede. - Hvor længe ændringsloggen gemmes i SharePoint Online, står ingen steder. Et meget gammelt token kan give et ufuldstændigt svar uden fejl.