• +351 91 33 888 29
    • clico@clico.pt

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

    Espelhar listas de Sharepoint com Power Automate

    Guia Prático: Espelhar Todas as Listas do 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.

    clicopt

    Deixe a sua mensagem

    Advertisements
    Show Buttons
    Hide Buttons