Cuando necesitamos eliminar cientos o miles de registros en una lista de SharePoint, nos enfrentamos a varios retos: límites de ejecución, errores de “throttling”, lentitud, y procesos poco fiables. Y si encima quieres mantener control, eficiencia y trazabilidad… la cosa se complica.
Hace unos días me encontré con la necesidad de eliminar más de 20.000 elementos en una lista con más de 150 columnas. Lo intenté de forma directa con conectores nativos de sharepoint pero el proceso era muy rápido y, sobre todo, consumía cuota de asignación de mi plan de Automate. Al final, tuve que investigar alternativas y encontrar la forma de eliminar «más con menos».
La solución llegó diseñando un flujo por lotes, controlado, usando la capacidad de API BATCH de SharePoint. En este post te explico cómo lo monté paso a paso, y también como he tenido que buscar documentación y consultar en foros y comunidades para dar con la solución.
En primer lugar os mostraré una imagen del flujo completo y luego iremos analizando cada acción:


PRIMERA PARTE: CONFIGURACIÓN INICIAL DEL FLUJO
1. Crear un contador de iteraciones

Lo primero que hacemos es inicializar una variable llamada Contador, de tipo Entero, con valor inicial 0.
Esta variable nos permite llevar la cuenta de cuántos ciclos de eliminación hemos ejecutado. Esta variable es fundamental para el control del flujo, ya que se utiliza dentro del bucle Do until para gestionar la cantidad de ciclos de eliminación que se ejecutan. Es especialmente útil cuando queremos asegurarnos de que el flujo no se quede en bucle indefinidamente por un error inesperado en los datos o en la respuesta de SharePoint.
2. Inicializar el último ID eliminado

A continuación, creamos otra variable llamada UltimoIDEliminado, de tipo Entero, con valor vacío (0).
Esta variable será clave para el control de paginación manual. En cada iteración, recuperaremos solo los elementos de la lista de SharePoint que tengan un ID mayor que este valor. Así:
- Evitamos eliminar el mismo elemento más de una vez.
- Garantizamos que el flujo puede retomarse con seguridad si falla a mitad.
3. Definir los parámetros base del flujo

Aquí llega una de las mejores prácticas del flujo: centralizar los valores clave en una única acción «Redactar» llamada Parametros.
En este Compose definimos lo siguiente:
{
"URL": "https://sites/SIGNUM_GROUP/",
"LISTA": "LISTA_PRUEBA",
"LOTE": "900"
}
Estos tres valores nos van a servir durante todo el flujo:
URL: la dirección del sitio SharePoint.LISTA: el nombre de la lista que estamos procesando.LOTE: el número de elementos que queremos procesar por bloque.
Además, dentro de esta acción se definen dos propiedades adicionales (trackedProperties) usando guid():

{
"IDLote": "@{guid()}",
"IDLoteCambios": "@{guid()}"
}
Estos identificadores únicos nos servirán para construir correctamente los bloques --batch_ y --changeset_ que SharePoint exige en las operaciones por lotes.
4. Crear la plantilla del bloque DELETE

En lugar de construir el cuerpo de cada instrucción DELETE a mano dentro del ciclo, definimos una plantilla reutilizable mediante otra acción de datos «Redactar» a la que vamos a llamar Plantilla.
Esta plantilla tiene el siguiente formato:
--changeset_@{actions('Parametros')?['trackedProperties']['IDLoteCambios']}
Content-Type: application/http
Content-Transfer-Encoding: binary
DELETE @{outputs('Parametros')['URL']}_api/web/lists/getByTitle('@{encodeURIComponent(outputs('Parametros')['LISTA'])}')/items(|ID|) HTTP/1.1
Content-Type: application/json;odata=verbose
Accept: application/json;odata=nometa
IF-MATCH: *
Esta plantilla es un fragmento válido del formato API BATCH de SharePoint. Durante el flujo, la iremos replicando con los IDs que queremos eliminar, concatenándolos todos en un solo cuerpo para enviar un lote completo.
Esta estrategia hace el flujo mucho más eficiente: en lugar de 1 llamada por registro, hacemos 1 llamada por 900 eliminaciones.
SEGUNDA PARTE: CICLO DE PROCESAMIENTO POR LOTES

5. Do until: repetimos hasta que no queden registros
Aquí usamos un bloque Do until, que ejecuta el ciclo completo de eliminación mientras sigan existiendo elementos en la lista.
La condición es simple: que el número de elementos recuperados en la iteración actual sea mayor que cero. Cuando Get items devuelve una colección vacía, el flujo termina.
6. Recuperar los registros de SharePoint
Usamos una acción Get items sobre la lista de SharePoint, primero usamos los parámetros de URL y LISTA y luego, con:
- Filtro:
ID gt @{variables('UltimoIDEliminado')} - Ordenación ascendente por ID (opcional, pero recomendable)
Top Counten 1000- Paginación activada, si la lista es de 20.000 entonces ponemos 25.000 o más.
Este paso trae solo los registros que no se han eliminado todavía.
Recuperar por ID creciente te da una garantía muy simple: si eliminas por bloques consecutivos, nunca te dejas nada atrás ni repites registros.
7. Extraer solo los IDs

Después, usamos una acción Select para quedarnos únicamente con el campo ID. Esto genera una colección limpia de enteros que usaremos para construir los bloques de borrado. Usamos esta expresión:
replace(outputs('Plantilla'), '|ID|', string(item()['Id']))
8. Dividir en bloques de 900 registros

Usamos otra acción Redactar y la expresión: chunk(body('Select'), 900) para dividir la colección de IDs en pequeños bloques de hasta 900 elementos.
¿Por qué 900 y no 1000?
Aunque SharePoint permite 1000 instrucciones por lote, dejar un margen de seguridad ayuda a evitar errores por tamaño excesivo en el cuerpo de la solicitud.
9. Recorrer cada bloque de 900 registros

Una vez dividida la colección de registros con chunk(), entramos en un bucle Apply to each que procesa cada lote individualmente.
Este bucle recorre los bloques generados previamente por la función chunk(body('Select'), 900).
Cada uno de estos bloques contiene hasta 900 elementos, y a continuación vamos a construir, ejecutar y registrar el lote DELETE que eliminará esos registros en SharePoint.
10. Construir el cuerpo del lote (acción Eliminar)
Dentro del bucle Apply to each, lo primero que hacemos es construir el cuerpo del lote utilizando una acción Redactar llamada EliminarLote.
Aquí usamos la función join() para unir cada una de las líneas DELETE que habíamos preparado, separándolas con un salto de línea (%0A):
@join(items('Aplicar_a_cada_uno'), decodeUriComponent('%0A'))
¿Qué conseguimos con esto?
Generamos un único bloque de texto que contiene hasta 900 instrucciones DELETE perfectamente formateadas, una detrás de otra, listas para ser enviadas a SharePoint como un único changeset.
11. Obtener el ID máximo procesado (acción IDMaximo)

Después de construir el lote, necesitamos identificar hasta qué punto hemos eliminado. Para eso usamos otra acción Redactar llamada IDMaximo con esta fórmula:
@coalesce(last(body('Get_items')?['value'])?['Id'], 0)
Esta expresión recupera el ID del último registro devuelto por la consulta Get items. Si por alguna razón no hay ningún valor (caso extremo), devuelve 0 por defecto.
Este paso es clave: nos permite saber cuál fue el último registro eliminado con éxito en el lote actual, y utilizarlo como punto de partida en la siguiente iteración del ciclo Do until.
12. Actualizar el valor de UltimoIDEliminado (acción Establecer_variable_2)

Con el IDMaximo calculado, usamos una acción Set variable para actualizar la variable UltimoIDEliminado:
Set variable UltimoIDEliminado = outputs('IDMaximo')
Así, cuando el ciclo vuelva a comenzar, solo se recuperarán los elementos con ID mayor al que ya ha sido eliminado, asegurando que no hay duplicados ni omisiones.
13. Enviar el lote a SharePoint (acción EnviarLoteSharePoint)

Una vez generado el cuerpo del lote con las instrucciones DELETE, lo enviamos a SharePoint mediante una acción de tipo “Enviar una solicitud HTTP a SharePoint”, configurada para realizar una operación POST sobre la ruta del endpoint batch. La URI se construye dinámicamente con la expresión @{outputs('Parametros')?['URL']} y se apunta directamente al servicio _api/$batch del sitio definido en la acción Parametros.
En los encabezados HTTP, se especifica el tipo de contenido como multipart/mixed, incluyendo un boundary dinámico que identifica el lote. La fórmula exacta del encabezado Content-Type es:
multipart/mixed; boundary=batch_@{actions('Parametros')?['trackedProperties']['IDLote']}
También se incluye el encabezado X-RequestDigest con el valor "digest", necesario para autenticar correctamente la llamada.
El cuerpo de la solicitud es más complejo, pero sigue un patrón preciso. Comienza con:
--batch_@{actions('Parametros')?['trackedProperties']['IDLote']}
Este marcador indica el inicio del lote. A continuación, se define el bloque changeset, usando otro identificador único generado también en Parametros:
Content-Type: multipart/mixed; boundary="changeset_@{actions('Parametros')?['trackedProperties']['IDLoteCambios']}"
Content-Length: @{length(outputs('EliminarLote'))}
Content-Transfer-Encoding: binary
Después de estos encabezados, se insertan todas las instrucciones DELETE generadas previamente en la acción EliminarLote:
@{outputs('EliminarLote')}
Y finalmente se cierran tanto el bloque changeset como el lote completo con:
--changeset_@{actions('Parametros')?['trackedProperties']['IDLoteCambios']}--
--batch_@{actions('Parametros')?['trackedProperties']['IDLote']}--
Este formato cumple con todos los requisitos del API BATCH de SharePoint: usa delimitadores únicos por cada bloque, define los encabezados correctamente y encapsula todas las operaciones DELETE en un solo envío.
Al construir esta estructura, conseguimos ejecutar hasta 900 eliminaciones en una única llamada, lo que mejora drásticamente el rendimiento frente a ejecutar acciones individuales. Además, al usar identificadores únicos con guid() en cada ciclo, garantizamos que no haya colisiones entre bloques y que SharePoint procese cada lote de forma segura y aislada.
Y esto es todo. Espero que más o menos lo haya conseguido explicar con la claridad necesaria, aunque es un flujo complicado.
Me gustaría agradecer todos los recursos a los que he tenido acceso y de los que he podido aprender y a todas las personas que, al igual que yo, trabajan por la comunidad y buscan compartir con el objetivo de generar conocimiento y sinergias y no solo obtener negocio.
