Sincronizar Esquemas de Listas do SharePoint com Power Automate (Multi-Ambiente)
Quer manter todas as listas dos seus sites de destino com o mesmo esquema (colunas) que as listas de um site de origem? Neste guia mostro o flow que uso para:
- Percorrer todas as listas e bibliotecas do site de origem;
- Criar a lista/biblioteca no destino se ainda não existir;
- Comparar campos (por InternalName) e criar automaticamente os que faltam;
- Lidar com vários destinos (ambientes diferentes);
- Tratar Lookups com mapeamento por ambiente.
Sem conectores premium, apenas Send an HTTP request to SharePoint e ações standard. 👇
Quando usar (e quando não)
Use quando precisa de manter consistência de esquema entre ambientes (Dev/Test/Prod) sem trabalho manual.
Não use para migrar dados (linhas) nem para tipos muito específicos como Managed Metadata complexos — podem ficar para uma fase 2.
Arquitetura do Flow
- Trigger: Recurrence (ex.: de hora a hora).
- Origem: 1 site SharePoint.
- Destino(s): 1..N sites SharePoint.
- Estratégia:
- Listar todas as listas/bibliotecas visíveis na origem;
- Para cada destino:
- Garantir que a lista/biblioteca existe (cria se faltar);
- Obter campos da origem (apenas custom);
- Obter campos do destino;
MissingFields = origem − destino
;- Criar MissingFields com POST à API (sem XML) — e XML/ajustes só quando necessário (Lookup, etc.).
Pré-requisitos
- Conta de serviço com permissões para ler e alterar esquema nos sites de origem e destino(s).
- Conector Send an HTTP request to SharePoint configurado nos tenants necessários.
- Naming consistente de listas (ou use o caminho server-relative).
Variáveis base (Initialize)
SourceSiteUrl
(String) → URL do site origem
Ex.:https://<tenant>.sharepoint.com/sites/Origem
TargetSites
(Array) → destinos + (opcional) mapas por ambiente:
[
{
"siteUrl": "https://<tenantA>.sharepoint.com/sites/DestinoA",
"lookupMap": {
"ClienteId": { "TargetListServerRelativeUrl": "/sites/DestinoA/Lists/Clientes" }
}
},
{
"siteUrl": "https://<tenantB>.sharepoint.com/sites/DestinoB",
"lookupMap": { }
}
]
CurrentSiteUrl
(String) =""
CurrentListTitle
(String) =""
CurrentLookupMap
(Object) ={}
TargetInternalNames
(Array) =[]
Dica: pode guardar TargetSites
num ficheiro JSON numa biblioteca e lê-lo com “Get file content”.
Passo 1 — Listar listas/bibliotecas na origem
HTTP (GET) – Get_All_Lists
- Site Address:
@{variables('SourceSiteUrl')}
- Headers:
Accept: application/json;odata=nometadata
- URI:
_api/web/lists?$select=Title,BaseTemplate,Hidden,RootFolder/ServerRelativeUrl,Id
&$expand=RootFolder
&$filter=(Hidden eq false) and ((BaseTemplate eq 100) or (BaseTemplate eq 101))
&$top=5000
Apply to each – ForEach_SourceLists
From → @{body('Get_All_Lists')?['value']}
- Compose – SourceListTitle →
@{items('ForEach_SourceLists')?['Title']}
- Compose – SourceListUrl →
@{items('ForEach_SourceLists')?['RootFolder']?['ServerRelativeUrl']}
BaseTemplate 100
= Lista; 101
= Biblioteca. Adicione outros se precisar.
Passo 2 — Para cada destino
Apply to each – ForEach_TargetSites
From → @{variables('TargetSites')}
- Set var – CurrentSiteUrl →
@{items('ForEach_TargetSites')?['siteUrl']}
- Set var – CurrentLookupMap →
@{items('ForEach_TargetSites')?['lookupMap']}
- Set var – CurrentListTitle →
@{outputs('SourceListTitle')}
2.1 Garantir que a lista existe
HTTP (GET) – Get_Target_List_By_Title
- Site:
@{variables('CurrentSiteUrl')}
- Headers:
Accept: application/json;odata=nometadata
- URI:
_api/web/lists?$filter=Title eq '@{replace(variables('CurrentListTitle'),'''','''''')}'
&$select=Id,Title,BaseTemplate&$top=1
Condition – ListExists → @greater(length(body('Get_Target_List_By_Title')?['value']), 0)
If NO → HTTP (POST) – Create_List
- Site:
@{variables('CurrentSiteUrl')}
- Headers:
Accept: application/json;odata=verbose
+Content-Type: application/json;odata=verbose
- URI:
_api/web/lists
- Body (Expression):
@json(
concat(
'{"__metadata":{"type":"SP.List"},"Title":"',
replace(variables('CurrentListTitle'),'"','\"'),
'","BaseTemplate":',
string(items('ForEach_SourceLists')?['BaseTemplate']),
',"AllowContentTypes":true,"ContentTypesEnabled":true}'
)
)
Se preferir criar por caminho (para bibliotecas com nomes localizados), também pode usar _api/web/folders
+ RootFolder (extra opcional).
Passo 3 — Obter campos da origem (custom)
HTTP (GET) – Get_Source_Fields
- Site:
@{variables('SourceSiteUrl')}
- Headers:
Accept: application/json;odata=nometadata
- URI:
_api/web/GetList(@listUrl)/Fields
?$select=Title,InternalName,TypeAsString,SchemaXml,Hidden,ReadOnlyField,Sealed,FromBaseType,CanBeDeleted,Required
&@listUrl='@{outputs('SourceListUrl')}'
Filter array – Keep_Custom_Fields
From: @{body('Get_Source_Fields')?['value']}
@and(
equals(item()?['Hidden'], false),
equals(item()?['FromBaseType'], false),
equals(item()?['Sealed'], false),
equals(item()?['ReadOnlyField'], false)
)
Select – Project_Source_Fields
From: @{body('Keep_Custom_Fields')}
Map: InternalName
, Title
, TypeAsString
, Required
, SchemaXml
.
Passo 4 — Obter campos do destino & calcular “MissingFields”
- Set var – TargetInternalNames →
[]
(limpar)
HTTP (GET) – Get_Target_Fields
- Site:
@{variables('CurrentSiteUrl')}
- Headers:
Accept: application/json;odata=nometadata
- URI:
_api/web/lists/getbytitle('@{replace(variables('CurrentListTitle'),'''','''''')}')
/Fields?$select=InternalName
Apply to each – ForEach_TargetFields
From: @{body('Get_Target_Fields')?['value']}
→ Append to array – TargetInternalNames = @{item()?['InternalName']}
Filter array – MissingFields
From: @{body('Project_Source_Fields')}
@not(contains(variables('TargetInternalNames'), item()?['InternalName']))
Passo 5 — Criar MissingFields
Apply to each – CreateMissingFields
From: @{body('MissingFields')}
Switch – On → @{items('CreateMissingFields')?['TypeAsString']}
Cabeçalhos e Endpoint (iguais em todos os cases)
- Site Address:
@{variables('CurrentSiteUrl')}
- URI:
_api/web/lists/getbytitle('@{replace(variables('CurrentListTitle'),'''','''''')}')/Fields
- Headers:
Accept: application/json;odata=verbose
Content-Type: application/json;odata=verbose
Bodies como Expression (@json(concat(...))
)
Text
@json(concat('{"__metadata":{"type":"SP.FieldText"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":2,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"MaxLength":255}'))
Note (multilinha)
@json(concat('{"__metadata":{"type":"SP.FieldMultiLineText"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":3,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"NumberOfLines":6,"RichText":false,"AppendOnly":false}'))
Number
@json(concat('{"__metadata":{"type":"SP.FieldNumber"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":9,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),'}'))
DateTime
@json(concat('{"__metadata":{"type":"SP.FieldDateTime"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":4,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"DisplayFormat":0}'))
Boolean
@json(concat('{"__metadata":{"type":"SP.FieldBoolean"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":8,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"DefaultValue":"0"}'))
User
@json(concat('{"__metadata":{"type":"SP.FieldUser"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":20,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"AllowMultipleValues":false,"SelectionMode":0}'))
URL
@json(concat('{"__metadata":{"type":"SP.FieldUrl"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":11,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"DisplayFormat":0}'))
Choice (ajuste as opções)
@json(concat('{"__metadata":{"type":"SP.FieldChoice"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":6,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"Choices":{"results":["Opção 1","Opção 2","Opção 3"]},"EditFormat":0}'))
MultiChoice
@json(concat('{"__metadata":{"type":"SP.FieldMultiChoice"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":15,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"Choices":{"results":["A","B","C"]},"FillInChoice":false}'))
Precisa de ajudaLookup / LookupMulti
- HTTP (GET) – GetRefListId
Site:@{variables('CurrentSiteUrl')}
| Headers:Accept: application/json;odata=nometadata
URI (por caminho, usando CurrentLookupMap):
_api/web/GetList(@listUrl)?$select=Id
&@listUrl='@{variables('CurrentLookupMap')?[items('CreateMissingFields')?['InternalName']]?['TargetListServerRelativeUrl']}'
Compose – RefListId → @{body('GetRefListId')?['Id']}
Lookup (simples)
@json(concat('{"__metadata":{"type":"SP.FieldLookup"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":7,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"LookupList":"{',outputs('RefListId'),'}","LookupField":"Title","AllowMultipleValues":false}'))
LookupMulti
@json(concat('{"__metadata":{"type":"SP.FieldLookup"},"Title":"',replace(items('CreateMissingFields')?['Title'],'"','\"'),'","StaticName":"',items('CreateMissingFields')?['InternalName'],'","FieldTypeKind":7,"Required":',if(items('CreateMissingFields')?['Required'],'true','false'),',"LookupList":"{',outputs('RefListId'),'}","LookupField":"Title","AllowMultipleValues":true}'))
Se não existir entrada no CurrentLookupMap
para aquele campo → não substitua (crie como simples) ou salte com log.
Boas práticas (o que funciona melhor)
- Compare por InternalName, não por Title.
- Faça escape de apóstrofos em
getbytitle
:replace(..., '''', '''''')
. - Prefira
GetList(@listUrl)
para bibliotecas/nomes localizados. - Controle de concorrência no loop de criação (1–3) para evitar throttling.
- Crie logs (site/lista/campo/resultado) numa lista “AdminLogs”.
- Use guard-rails: Conditions para só tratar Lookup quando houver
lookupMap
. - Faça um teste “seco”: primeiro calcule os
MissingFields
e escreva log; depois ative a criação.
Limitações & extensões
- Managed Metadata (Taxonomia) e Calculated complexos: recomendo criar via
SchemaXml
(ou PnP/CLI) numa segunda fase. - Renomeações (Title): para sincronizar títulos, adicione um PATCH/MERGE para
.../Fields/getbyinternalnameortitle('INTERNAL')
com"Title":"Novo Título"
. - Content Types: em ambientes com content types partilhados, pode ser preferível publicar/associar content types em vez de mexer diretamente em cada lista.
Conclusão
Com este flow passa a ter governança de esquema: qualquer lista nova/alterada na origem é replicada para todos os destinos, sem cliques manuais. A peça-chave é usar a API do SharePoint para listar, comparar e criar campos de forma segura, com variações para Lookups e bibliotecas.