Why orchestrate the sync
Pulling from an API on a schedule, handling pagination, converting the payload, indexing it, and tracking what you’ve already seen is exactly the kind of multi-step, stateful, must-not-silently-die job orchestration exists for. Kestra gives you the HTTP client, the format converters, a schedule, a KV store for watermarks, retries, and logging, all declaratively.Prerequisites
A running Kestra with the Meilisearch and serdes plugins (the HTTP client is built into Kestra core), plus a Meilisearch Cloud project. Only Kestra runs locally, since Meilisearch is managed:Get your Cloud credentials. In the Meilisearch Cloud dashboard, create a project and copy its Project URL (the
url in the flows below) and its Default Admin API Key (Settings, then API Keys). Keep that key and any upstream API tokens in Kestra secrets, referenced as {{ secret('NAME') }}.Step 1: The first load (backfill)
The pipeline is three steps: download the JSON from the API, convert it to Kestra’s ION format, index it. Here’s the pattern against a public API:http.Download fetches the response into internal storage, JsonToIon converts it, and DocumentAdd indexes it. If the API needs authentication, add headers:
plugin-transform-json plugin.) Make sure each document has a field Meilisearch can use as a primary key.
Pagination. For APIs that page, wrap the download in a loop that walks pages until the response is empty, appending each page’s documents. Kestra’s ForEach/EachSequential tasks or a paginating HTTP pattern handle this; index each page as you go so memory stays flat.
Step 2: Incremental sync
This is where REST APIs differ from databases: you can’t run aWHERE updated_at > X query unless the API gives you one. Two cases.
Case A: the API supports “changed since”
Many well-designed APIs accept a filter like?updated_since= or ?modified_after=. When yours does, incremental sync is clean: on each run, ask only for what changed since the last successful run. Kestra exposes the previous execution’s scheduled time, and its KV store can persist a precise watermark.
Using the schedule’s own trigger date as the lower bound:
io.kestra.plugin.core.kv.Get, use it in the query, and write the new high-water mark with io.kestra.plugin.core.kv.Set after a successful index. Overlap is safe regardless: DocumentAdd is add-or-replace, so re-fetching a few already-seen records just overwrites them by primary key.
Case B: the API has no change filter
When the API can only return the full collection, you have two honest options:- Periodic full re-index. Just run the Step 1 backfill on a schedule. It’s the simplest thing that works, and because upserts are idempotent it’s completely safe: every run reconciles the index to the current API state. Fine for small-to-medium collections.
- Diff against a fresh index plus alias swap. Index each full pull into a new index, then atomically point a Meilisearch alias at it. This also handles deletes for free (anything no longer returned by the API simply isn’t in the new index), at the cost of re-indexing everything each time.
Handling deletes
Deletes are the hard part with REST sources, because most APIs don’t tell you what was removed: a record just stops appearing.- If the API exposes deletions (a
deletedflag, a/deletionsendpoint, or a status field), fetch those ids and call Meilisearch’sdocuments/delete-batchendpoint with anhttp.Requesttask. The exact pattern is in Connect PostgreSQL to Meilisearch with Kestra. - If it doesn’t, use the fresh-index-plus-alias-swap approach from Case B. It’s the only reliable way to drop records the API no longer returns.
Going to production
- Rate limits. Add a
retryblock with backoff so429responses are retried politely; space out pagination requests if the API is strict. - Watermark durability. Prefer the KV-store watermark over the schedule date when you need exactly-once semantics. It survives missed runs and backfills correctly after downtime.
- Auth refresh. If the API uses short-lived tokens, add a first task that fetches a fresh token (another
http.Request) and pass it downstream. - Choose the interval by freshness need. A 15-minute schedule is a sensible default; tighten or loosen it to match how fast the source changes and the API’s rate limits.