<?xml version="1.0" encoding="UTF-8"?>
<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns:taxo="http://purl.org/rss/1.0/modules/taxonomy/" version="2.0">
  <channel>
    <title>article Declarative Pipelines meets foreachBatch: Custom Streaming for Advanced Pipelines in Technical Blog</title>
    <link>https://community.databricks.com/t5/technical-blog/declarative-pipelines-meets-foreachbatch-custom-streaming-for/ba-p/142033</link>
    <description>&lt;P&gt;&lt;SPAN&gt;If this is the first time you’re hearing about Sinks in Lakeflow Declarative Pipelines (LDP), we highly recommend reading &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/introducing-dlt-sink-api-write-pipelines-kafka-and-external-delta-tables" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Introducing the SDP Sink API&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;, which explores the recently launched Sinks API, offering the ability to seamlessly sink data to external systems, such as event streams and Delta tables.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Introduction&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;LDP provides a declarative framework designed to simplify and streamline pipeline development, whether you're processing real-time data with Streaming Tables or aggregating data efficiently using Materialized Views. The introduction of the Sinks API further enhances functionality by enabling seamless integration with external systems.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;This blog delves into the support for &lt;/SPAN&gt;&lt;A href="https://docs.databricks.com/aws/en/ldp/for-each-batch" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;foreachBatch in LDP&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;, a feature from Apache Spark Structured Streaming that facilitates arbitrary batch operations on streaming data, enabling complex transformations and writes to various data sinks.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;From Clicks to Insights: Clickstream Data in Action&lt;/SPAN&gt;&lt;/H1&gt;
&lt;P&gt;&lt;SPAN&gt;For simplicity and reproducibility, this blog post uses the same data source as the &lt;/SPAN&gt;&lt;A href="https://www.databricks.com/blog/introducing-dlt-sink-api-write-pipelines-kafka-and-external-delta-tables" target="_blank" rel="noopener"&gt;&lt;SPAN&gt;Sinks API blog&lt;/SPAN&gt;&lt;/A&gt;&lt;SPAN&gt;: the clickstream dataset from Databricks Datasets. You can think of this data as a map of the internet, illustrating where visitors navigate and how they move from one page to another. To keep things straightforward, we'll assume a one-to-one relationship between page IDs and titles, with titles remaining static. Using this dataset, we'll explore three key use cases:&lt;/SPAN&gt;&lt;/P&gt;
&lt;OL&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;The Comprehensive Visit Aggregator: Let's dive deeper into total page visits by aggregating them regardless of origin to uncover popular destinations.&amp;nbsp;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;The Million-Click Club: This is where things get interesting. We'll implement a monitoring system that records data in SQL Server whenever page visits exceed the one-million mark within a single time unit.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI style="font-weight: 400;" aria-level="1"&gt;&lt;SPAN&gt;The New York City Tracker: In the final step, we'll actively monitor visits to the "New_York_City" page. Whenever the incoming stream captures such an event, we’ll archive the data as Parquet records for efficient storage and analysis.&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/OL&gt;
&lt;P&gt;&lt;SPAN&gt;By the end of this post, you’ll see how LDP can elevate your data processing workflow. We’ll walk through how to efficiently handle streaming data in real time, seamlessly merge it with existing Delta tables, and dynamically route it to multiple destinations!&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Code Walkthrough&lt;/SPAN&gt;&lt;/H1&gt;
&lt;H2&gt;&lt;SPAN&gt;Preparing Clickstream Data&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;We’ll start by importing the necessary modules and creating a Streaming Table to ingest Wikipedia clickstream data in JSON format. Using the &lt;/SPAN&gt;&lt;STRONG&gt;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/25059"&gt;@DP&lt;/a&gt;.table&lt;/STRONG&gt;&lt;SPAN&gt; API, we’ll stream data directly from the source location. You can also configure the pipeline to stream data from any source supported by Autoloader, including event streams. It’s as simple as switching the format and providing the necessary authorization.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;from pyspark import pipelines as dp
from pyspark.sql.functions import *
from pyspark.sql.types import *
from delta.tables import DeltaTable


json_path = "/databricks-datasets/wikipedia-datasets/data-001/clickstream/raw-uncompressed-json/"


@dp.table(
 comment="The raw wikipedia click stream dataset, ingested from /databricks-datasets.",
 table_properties={
   "quality": "bronze"
 }
)
def clickstream_raw():
 return (
   spark.readStream.format("cloudFiles").option("cloudFiles.format", "json").option("inferSchema", "true").load(json_path)
 )&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;With our clickstream data now streaming into the raw table, the next step is to refine it by applying data quality constraints and transformations. This includes standardizing data types and filtering out invalid records. We achieve this by leveraging LDP’s native expectations to enforce data quality and maintain pipeline integrity.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;@DP.table(
 comment="Wikipedia clickstream dataset with cleaned-up datatypes / column names and quality expectations.",
 table_properties={
   "quality": "silver"
 }
)
@dp.expect_or_drop("valid_current_page", "current_page_id IS NOT NULL AND current_page_title IS NOT NULL")
@dp.expect_or_fail("valid_count", "click_count &amp;gt; 0")
def clickstream_clean():
 return (
   dp.readStream("clickstream_raw")
     .withColumn("current_page_id", expr("CAST(curr_id AS INT)"))
     .withColumn("click_count", expr("CAST(n AS INT)"))
     .withColumn("previous_page_id", expr("CAST(prev_id AS INT)"))
     .withColumnRenamed("curr_title", "current_page_title")
     .withColumnRenamed("prev_title", "previous_page_title")
     .select("current_page_id", "current_page_title", "click_count", "previous_page_id", "previous_page_title")
 )&lt;/LI-CODE&gt;
&lt;H2&gt;&lt;SPAN&gt;foreachBatch - Writing to multiple sinks&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;With our cleaned data in a Streaming Table, we can now implement our three use cases using &lt;/SPAN&gt;&lt;STRONG&gt;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/25059"&gt;@DP&lt;/a&gt;.foreach_batch_sink&lt;/STRONG&gt;&lt;SPAN&gt;, LDP's new sink that brings foreachBatch-style processing into LDP. This allows streaming data to be handled as microbatches while integrating with regular DataFrame APIs.&amp;nbsp;&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;In Spark Structured Streaming, data is processed as a series of micro-batches. Each micro-batch contains a specific set of rows, represented as a DataFrame. During processing, this DataFrame, along with the corresponding &lt;/SPAN&gt;&lt;STRONG&gt;batchId&lt;/STRONG&gt;&lt;SPAN&gt;, is passed to the function registered with the sink. We will use the DataFrame &lt;/SPAN&gt;&lt;STRONG&gt;df&lt;/STRONG&gt;&lt;SPAN&gt;, which contains the rows from a micro-batch, to implement our use cases.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;In the first use case, we aggregate total page visits at the sink across all origins. Since each micro-batch may contain multiple rows for the same &lt;/SPAN&gt;&lt;STRONG&gt;current_page_id&lt;/STRONG&gt;&lt;SPAN&gt;, we first aggregate the data at the micro-batch level. If an existing value is present in the sink, we update it accordingly; otherwise, we insert a new record.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;The second use case filters the &lt;/SPAN&gt;&lt;STRONG&gt;click_count&lt;/STRONG&gt;&lt;SPAN&gt; to identify high-traffic pages within each time interval and appends this data to a table called &lt;/SPAN&gt;&lt;STRONG&gt;dbo.high_traffic_pages&lt;/STRONG&gt;&lt;SPAN&gt; in a SQL Server database for consumption by external systems. We are leveraging Databricks Secrets to store the connection string and utilize that for authentication with SQL Server.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;For the final use case, we’ll apply a simple filter on the DataFrame to capture page visits to New York City and securely archive this data as parquet files to a volume in Unity Catalog. Access to the archived data can be managed and controlled directly through the Unity Catalog.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;@DP.foreach_batch_sink(name = "all_the_sinks")
def foreachBatchFunc(df, batchId):
 #Usecase 1: Aggregate page level click count at a Delta Sink
 agg_df = df.groupBy("current_page_id", "current_page_title").agg(sum("click_count").alias("click_count"))
 out = DeltaTable.forName(df.sparkSession, "harsha_pasala.default.clickstream_sink")
 out.alias("target") \
   .merge(agg_df.alias("source"), "source.current_page_id = target.current_page_id") \
   .whenMatchedUpdate(
     set = {"click_count": col("target.click_count") + col("source.click_count")}
   ) \
   .whenNotMatchedInsert(
     values = {
       "target.current_page_id": "source.current_page_id",
       "target.current_page_title": "source.current_page_title",
       "target.click_count": "source.click_count"
     }
   ) \
   .execute()


 #Usecase 2: High traffic pages to SQL Server.
 high_traffic_pages = df.filter(col("click_count") &amp;gt; 1000000)
 high_traffic_pages.write \
   .format("jdbc") \
   .option("url", dbutils.secrets.get(scope="secret-lab", key="sql_connection_string")) \
   .option("dbtable", "dbo.high_traffic_pages") \
   .mode("append") \
   .save()


 #Usecase 3: Monitor for a specific page and archive data.
 new_york_clicks = df.filter(col("current_page_title").like("New_York_City"))
 new_york_clicks.write.format("parquet").mode("append").save("/Volumes/path/")&lt;/LI-CODE&gt;
&lt;H2&gt;&lt;SPAN&gt;Streaming data to sinks&lt;/SPAN&gt;&lt;/H2&gt;
&lt;P&gt;&lt;SPAN&gt;Our data is ready, and the sink is in place - it is time to put it into action. Since our sinks modify data using the merge behaviour, we’ll use the &lt;/SPAN&gt;&lt;STRONG&gt;update_flow&lt;/STRONG&gt;&lt;SPAN&gt; API to direct data from the &lt;/SPAN&gt;&lt;STRONG&gt;clickstream_clean&lt;/STRONG&gt;&lt;SPAN&gt; Streaming Table to the &lt;/SPAN&gt;&lt;STRONG&gt;foreach_batch_sink&lt;/STRONG&gt;&lt;SPAN&gt; we created earlier, which is &lt;/SPAN&gt;&lt;STRONG&gt;all_the_sinks&lt;/STRONG&gt;&lt;SPAN&gt;. This ensures that data is efficiently processed and routed according to our defined use cases.&lt;/SPAN&gt;&lt;/P&gt;
&lt;LI-CODE lang="python"&gt;@DP.update_flow(
 target="all_the_sinks",
 name="sink_flow"
)
def read_data():
 return (
   dp.readStream("clickstream_clean")
 )&lt;/LI-CODE&gt;
&lt;P&gt;&lt;SPAN&gt;Finally, by integrating clickstream data ingestion, processing with quality constraints, and multiple sinks within a LDP pipeline, we arrive at the following DAG from the pipeline run. While the DAG displays a single sink named &lt;/SPAN&gt;&lt;SPAN&gt;all_the_sinks&lt;/SPAN&gt;&lt;SPAN&gt;, it's important to note that this represents a &lt;/SPAN&gt;&lt;STRONG&gt;foreach_batch_sink&lt;/STRONG&gt;&lt;SPAN&gt;, that effectively dispatches data to all three configured downstream targets.&lt;/SPAN&gt;&lt;/P&gt;
&lt;P&gt;&lt;span class="lia-inline-image-display-wrapper lia-image-align-inline" image-alt="unnamed (1).png" style="width: 999px;"&gt;&lt;img src="https://community.databricks.com/t5/image/serverpage/image-id/22351iBE2CB27863C24CD8/image-size/large?v=v2&amp;amp;px=999" role="button" title="unnamed (1).png" alt="unnamed (1).png" /&gt;&lt;/span&gt;&lt;/P&gt;
&lt;P&gt;&lt;SPAN&gt;Over the past year, LDP has introduced numerous enhancements, with &lt;/SPAN&gt;&lt;STRONG&gt;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/25059"&gt;@DP&lt;/a&gt;.foreach_batch_sink&lt;/STRONG&gt;&lt;SPAN&gt; being one of the latest additions. This feature enables advanced streaming capabilities while building on existing sinks and append flow APIs, allowing for a cleaner and more streamlined implementation. For more information about the API and sample implementations, please refer to the &lt;A href="https://docs.databricks.com/aws/en/ldp/for-each-batch" target="_self"&gt;docs&lt;/A&gt;.&lt;/SPAN&gt;&lt;/P&gt;
&lt;H1&gt;&lt;SPAN&gt;Related Databricks Blogs:&lt;/SPAN&gt;&lt;/H1&gt;
&lt;UL&gt;
&lt;LI&gt;&lt;SPAN&gt;&lt;A href="https://www.databricks.com/blog/introducing-dlt-sink-api-write-pipelines-kafka-and-external-delta-tables" target="_self"&gt;Introducing the DLT Sink API: Write Pipelines to Kafka and External Delta Tables&lt;/A&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN&gt;&lt;A href="https://www.databricks.com/discover/pages/getting-started-with-delta-live-tables" target="_self"&gt;Getting started with DLT&lt;/A&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN&gt;&lt;A href="https://www.databricks.com/blog/cost-effective-incremental-etl-serverless-compute-delta-live-tables-pipelines" target="_self"&gt;Cost-effective, incremental ETL with serverless compute for Delta Live Tables pipelines&lt;/A&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;LI&gt;&lt;SPAN&gt;&lt;A href="https://www.databricks.com/blog/2022/08/09/low-latency-streaming-data-pipelines-with-delta-live-tables-and-apache-kafka.html" target="_self"&gt;Low-latency Streaming Data Pipelines with Delta Live Tables and Apache Kafka&lt;/A&gt;&lt;BR /&gt;&lt;/SPAN&gt;&lt;/LI&gt;
&lt;/UL&gt;</description>
    <pubDate>Thu, 08 Jan 2026 14:22:41 GMT</pubDate>
    <dc:creator>harshapasala</dc:creator>
    <dc:date>2026-01-08T14:22:41Z</dc:date>
    <item>
      <title>Declarative Pipelines meets foreachBatch: Custom Streaming for Advanced Pipelines</title>
      <link>https://community.databricks.com/t5/technical-blog/declarative-pipelines-meets-foreachbatch-custom-streaming-for/ba-p/142033</link>
      <description>&lt;P data-start="365" data-end="811"&gt;&lt;STRONG data-start="365" data-end="479"&gt;What if a single Declarative Pipeline could apply multiple downstream actions to each micro-batch?&lt;/STRONG&gt;&lt;BR data-start="479" data-end="482" /&gt;With the new &lt;STRONG data-start="495" data-end="523"&gt;&lt;CODE data-start="497" data-end="521"&gt;@dp.foreach_batch_sink&lt;/CODE&gt;&lt;/STRONG&gt; in Lakeflow Declarative Pipelines, you can bring the power of Spark’s &lt;CODE data-start="594" data-end="608"&gt;foreachBatch&lt;/CODE&gt; into a declarative workflow. This post walks through a real world clickstream example, showing how each micro-batch can be sequentially aggregated, monitored, and archived using standard DataFrame APIs.&lt;/P&gt;</description>
      <pubDate>Thu, 08 Jan 2026 14:22:41 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/declarative-pipelines-meets-foreachbatch-custom-streaming-for/ba-p/142033</guid>
      <dc:creator>harshapasala</dc:creator>
      <dc:date>2026-01-08T14:22:41Z</dc:date>
    </item>
    <item>
      <title>Re: Declarative Pipelines meets foreachBatch: Custom Streaming for Advanced Pipelines</title>
      <link>https://community.databricks.com/t5/technical-blog/declarative-pipelines-meets-foreachbatch-custom-streaming-for/bc-p/143361#M874</link>
      <description>&lt;P&gt;Great writeup&amp;nbsp;&lt;a href="https://community.databricks.com/t5/user/viewprofilepage/user-id/46901"&gt;@harshapasala&lt;/a&gt;&amp;nbsp;,&amp;nbsp;&lt;/P&gt;
&lt;P class="p1"&gt;The @dp.foreach_batch_sink decorator is a great example of what I love about modern Databricks pipelines: declarative on the surface, but with real imperative muscle underneath. One sink, three destinations, and suddenly a whole new design space opens up. You can merge into Delta, route high-traffic events to SQL Server, and archive targeted data into Unity Catalog—all without breaking the declarative flow.&lt;/P&gt;
&lt;P class="p1"&gt;This is exactly the kind of flexibility streaming teams need when real-world requirements refuse to stay simple. For anyone juggling multi-destination streaming workflows, LDP just became a lot more compelling—and honestly, a lot more fun to work with.&lt;/P&gt;
&lt;P class="p1"&gt;Keep sharing your thoughts.&lt;/P&gt;
&lt;P class="p1"&gt;Regards, Louis.&lt;/P&gt;</description>
      <pubDate>Thu, 08 Jan 2026 14:28:57 GMT</pubDate>
      <guid>https://community.databricks.com/t5/technical-blog/declarative-pipelines-meets-foreachbatch-custom-streaming-for/bc-p/143361#M874</guid>
      <dc:creator>Louis_Frolio</dc:creator>
      <dc:date>2026-01-08T14:28:57Z</dc:date>
    </item>
  </channel>
</rss>

