Custom Sink

Use this when: no built-in or plugin sink matches the destination system.

Custom sinks subclass BaseSink[T].

Minimum shape

from agora import BaseSink


class MySink(BaseSink[dict]):
    sink_name = "my_sink"

    async def open(self) -> None:
        self._client = await connect()

    async def write(self, record: dict) -> None:
        await self._client.send(record)

    async def close(self) -> None:
        await self._client.close()

Every sink always accepts Python rows. Additional native planes are optional and advertised through the sink's public data-plane contract:

from agora import DataPlane, sink_data_plane_spec

spec = sink_data_plane_spec(MySink())
assert spec.accepted_planes == (DataPlane.PYTHON_ROWS,)
assert spec.native_planes == (DataPlane.PYTHON_ROWS,)

Native batch support

If the destination can write many records at once, implement write_batch() and advertise batch-native planes explicitly:

from agora import BaseSink, DataPlane


class MyBatchSink(BaseSink[dict]):
    sink_name = "my_batch_sink"
    accepted_data_planes = (
        DataPlane.PYTHON_ROWS,
        DataPlane.PYTHON_BATCHES,
    )
    native_data_planes = accepted_data_planes

    async def write_batch(self, records: list[dict]) -> None:
        await self._client.send_many(records)

That sink will now advertise:

  • accepted_planes = (python_rows, python_batches)
  • native_planes = (python_rows, python_batches)

Arrow boundary support

If the sink can accept Arrow batches directly at the writer/sink boundary, implement write_arrow_batch() and advertise Arrow in native_data_planes:

import pyarrow as pa

from agora import BaseSink, DataPlane


class MyArrowSink(BaseSink[dict]):
    sink_name = "my_arrow_sink"
    accepted_data_planes = (
        DataPlane.PYTHON_ROWS,
        DataPlane.PYTHON_BATCHES,
        DataPlane.ARROW_BATCHES,
    )
    native_data_planes = accepted_data_planes

    async def write(self, record: dict) -> None:
        await self._client.send_one(record)

    async def write_batch(self, records: list[dict]) -> None:
        await self._client.send_many(records)

    async def write_arrow_batch(self, batch: pa.RecordBatch) -> None:
        await self._client.send_arrow(batch)

When the upstream pipeline is on the Arrow chain:

  • sinks with Arrow support receive RecordBatch
  • sinks without Arrow support are downgraded at the sink boundary

For custom sinks, prefer checking sink_data_plane_spec(sink) in tests instead of asserting only on internal flags. That helper uses the same normalization path the runtime planner/writer uses.

In 0.4.0, Agora no longer infers sink planes from legacy bool flags. Declare accepted_data_planes and native_data_planes explicitly, and prefer sink_data_plane_spec(sink) in tests.

Practical advice

  • make writes idempotent if retries are possible
  • use native batch writes when the destination supports them
  • test sink_data_plane_spec() for every non-row sink
  • keep shutdown behavior explicit by flushing or closing resources in close()