Saltar para o conteúdo
Consultoria · Automação · Dados · Digital
← Blog

Guia Prático: Espelhar Todas as Listas do SharePoint com Power Automate

Espelhar listas de Sharepoint com Power Automate
Espelhar Listas SharePoint com Power Automate (Multi-Ambiente)

Espelhar Listas SharePoint com Power Automate (Multi-Ambiente)

Quer espelhar listas SharePoint e 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) para espelhar listas SharePoint com consistência;
  • 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 espelhar listas SharePoint para 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 para espelhar listas SharePoint

  • Trigger: Recurrence (ex.: de hora a hora).
  • Origem: 1 site SharePoint.
  • Destino(s): 1..N sites SharePoint.
  • Estratégia:
    1. Listar todas as listas/bibliotecas visíveis na origem;
    2. 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']))

Este passo assegura que consegue espelhar listas SharePoint criando apenas os campos em falta.

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 ajuda

Lookup / LookupMulti

  1. 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 para realmente espelhar listas SharePoint nos destinos.

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 espelhar listas SharePoint e garantir 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.

Da ideia à solução

Tem um desafio tecnológico ou digital para resolver?

Falamos sobre o contexto, identificamos opções e desenhamos uma abordagem pragmática.

Fale connosco
Show Buttons
Hide Buttons