New to Kestra?
Use blueprints to kickstart your first workflows.
Poll Oracle for new orders, enrich them, claim the exact id window race-safely, and MERGE into a mart so replays never duplicate.
Run a change-data-capture style incremental sync from an Oracle Database with Kestra. A polling trigger captures a bounded batch of new orders, enriches them with customer attributes, claims exactly that id window race-safely, and upserts the result into a reporting mart with a MERGE so a replay never duplicates. This is the idempotent, at-least-once pattern teams reach for when an Oracle scheduler job and a plain INSERT keep producing duplicate rows or skipping records under concurrency.
poll_new_orders trigger (io.kestra.plugin.jdbc.oracle.Trigger) polls every PT5M for orders
rows where processed_flag IS NULL, ordered by order_id, fetching only the first batch_size rows
(default 500). Its sql LEFT JOINs customers so each captured row already carries customer_name
and region, and fetchType: STORE writes the batch to internal storage as trigger.uri.afterSQL claims that exact window, marking only rows whose order_id is at or below the
highest id in the batch. Because order_id is monotonically increasing, rows inserted concurrently get
a higher id and are simply picked up next poll, avoiding the race a blanket UPDATE ... WHERE processed_flag IS NULL would create.stage_batch task (io.kestra.plugin.jdbc.oracle.Batch) bulk loads the enriched batch from
trigger.uri into the transient orders_enriched_stg table in chunks of batch_size.merge_into_mart task (io.kestra.plugin.jdbc.oracle.Queries) runs a MERGE into orders_enriched
keyed on order_id and clears the staging table in one transaction (transaction: true). The MERGE
makes replays idempotent.notify_batch task (io.kestra.plugin.slack.notifications.SlackIncomingWebhook) posts the synced
row count to Slack.MERGE on order_id absorbs replays instead of inserting duplicates.stage_batch, merge_into_mart, and notify_batch each retry (constant, 3 attempts).errors alert naming the execution id and
trigger.uri to replay.concurrency.limit is 1, so a slow mart load just delays the next poll.Oracle's own scheduler (DBMS_SCHEDULER) can run SQL on a clock, but it cannot store the captured batch as an artifact, retry individual steps, alert with the exact batch to replay, or give you execution-level lineage when a load half-fails. Kestra adds an event-driven polling trigger, per-task retries, a flow-level error handler, and declarative YAML you version in Git, turning a fragile "claim then load" job into a replayable, auditable pipeline.
jdbc:oracle:thin:@//host:1521/service), set in the oracle_url variable.orders table with a numeric, sequence-generated order_id and a processed_flag column, a
customers table to join against, an orders_enriched mart with a unique order_id, and an
orders_enriched_stg staging table of the same shape.orders and customers, UPDATE on processed_flag, and INSERT/UPDATE/DELETE
on the mart and staging tables.ORACLE_USERNAME: the Oracle database user used by the trigger and the mart load.ORACLE_PASSWORD: the password for that Oracle user.SLACK_WEBHOOK: the Slack Incoming Webhook URL the batch summary and failure alert post to.ORACLE_USERNAME, ORACLE_PASSWORD, and SLACK_WEBHOOK secrets.oracle_url variable to point at your instance and service name.orders_enriched_stg matching orders_enriched, with a unique constraint on orders_enriched.order_id.sql, the batch_size variable, the staging INSERT, and the MERGE to match your schema.batch_size or lowering the trigger interval (default drains about
6,000 rows per hour).last_modified timestamp instead of processed_flag for update-aware syncs.