Logistics & Travel

Billions of routes, prices, & shipments. In pure Python.

Demand forecasting, dynamic pricing, ETA models, network optimization. Union fans a single Python function out to thousands of concurrent tasks, so your team ships new models instead of babysitting a distributed cluster.

Try the devbox

A free, local sandbox to
explore the Union.ai platform.

Chat with an engineer

Trusted by leading logistics & travel teams

Built on Flyte

Open source at the core.

Union is built on Flyte, the open-source AI runtime we create and maintain under the Linux Foundation AI & Data.

4000+

companies using Flyte today

17M+

Flyte SDK downloads

Function-Level Checkpointing

Recover. Fork. Replay.

Pricing and routing pipelines run on a clock. A carrier feed lands late or malformed at two in the morning and the run dies four hours deep, and starting from the top means missing the window entirely. Union records what finished as the run happens, outside the node doing the work, and that record is what you resume from.

  • Recover what failed. Point `recover` at a prior run. A late feed for one lane reruns that lane, not the night, even without caching enabled.
  • Fork from any step. `flyte.rerun` starts a prior run again at the step you pick, with a corrected feed or a new pricing model.
  • Replay with today's code. `--rerun-from` runs a prior run's inputs against the code on your machine right now.
Infrastructure as Context

Except blocks can change the hardware.

Partition sizes in logistics are never even. One lane carries a hundred times the volume of the median, and a memory limit sized for the median dies on it while a limit sized for the peak overpays on every other partition. Union hands you a typed error instead of a stack trace, so the partition that blows memory comes back as an exception you can catch and re-run on a bigger box.

  • Typed infrastructure errors. `OOMError`,` TaskInterruptedError`, `TaskTimeoutError`, `ImagePullBackOffError`. Failures you can branch on.
  • Resources changed at runtime. `.override(resources=...)` re-runs the same partition with different memory, CPU, or accelerator.
  • Right machine per step. Cheap wide workers for ingest, a high-memory solver node for optimization, decided in code rather than in a cluster config.
  • The handler does not have to be human. Provisioning is an ordinary Python call, so the same API you write a retry policy against is one an agent can call while a run is in flight.
Fan-Out & Scale

Fan out on durable asyncio.

Hopper ran 22,300 tasks with 7,000 executing concurrently to visualize 4.4 billion trip records. Fanning out over lanes, markets, or dates is plain async, with no cluster semantics to reason about and no framework to fight.

  • Concurrency you can cap. `flyte.map.aio(fn, lanes, concurrency=500)` bounds how many run at once, for a fan-out wider than the cluster.
  • Retries with pacing. `retries=5`, or a `RetryStrategy` with exponential backoff so a flaky carrier API gets time to come back.
  • Built for spot. `interruptible=True` puts branches on spot nodes, and container reuse keeps startup cost off thousands of short-lived tasks.
Durable Artifacts

Outputs that outlive the run.

A demand forecast should not be a warehouse table whose name one person remembers. Artifacts are typed, versioned values that persist past the run that made them, so a routing workflow consumes this morning's price surface without re-running the model that produced it.

  • Passed between workflows and apps. One pipeline's forecast is another's typed input. `flyte.io.DataFrame` moves a pointer to object storage, not the rows.
  • Versioned, not overwritten. A new version is a new artifact, so a downstream run resolves to the exact forecast it priced against.
  • Events, not polling. `flyte.OnArtifact` fires a run whenever a new forecast version lands, with no cron hacks in between.
Run History & Versioning

Reproduce a run from months ago.

Someone is asking which forecast actually priced the network on peak weekend, and what it ran against. Every run keeps what it takes to reproduce a result: the code that executed, the infrastructure it ran on, and the configuration applied to both. That is what makes a pipeline shareable rather than personal, and it is the same record an audit or a postmortem needs.

  • Inputs and outputs per run. Open an execution from months ago and see exactly which feeds went in and which prices came out.
  • The code that ran, not the code today. Each run resolves to its own code bundle and container image, down to the solver version.
  • Full lineage for every forecast you shipped. Versioned workflows, containers, and artifacts, traced back to the partitions that produced them.
Pure Python

Network optimization in pure Python.

Ingest, enrich, forecast, optimize. No YAML, no DSLs. Write it in Python, run it across thousands of workers on Union.

import asyncioimport flyte # Wide, cheap workers for ingest and enrichmentetl_env = flyte.TaskEnvironment(    name="lane-etl",    image=flyte.Image.from_debian_base().with_pip_packages("pandas", "pyarrow"),    resources=flyte.Resources(cpu="2", memory="8Gi"),    reusable=flyte.ReusePolicy(replicas=200, concurrency=4),) # Fat memory nodes for the solversolve_env = flyte.TaskEnvironment(    name="route-solver",    image=flyte.Image.from_debian_base().with_pip_packages("ortools", "pandas"),    resources=flyte.Resources(cpu="16", memory="64Gi"),    depends_on=[etl_env],)  @etl_env.task(cache="auto", retries=3)async def load_lane(lane: str, day: str) -> flyte.io.DataFrame:    """Pull one lane-day of shipments from the warehouse."""    df = query_warehouse(lane, day)    return flyte.io.DataFrame.from_df(enrich_with_weather(df))  @etl_env.task(cache="auto")async def forecast(history: flyte.io.DataFrame, horizon: int) -> flyte.io.DataFrame:    """Forecast demand per lane. Cached on lane-day inputs."""    return flyte.io.DataFrame.from_df(        predict_demand(await history.open(), horizon)    )  @solve_env.taskasync def optimize(demand: list[flyte.io.DataFrame]) -> flyte.io.File:    """Solve the network plan against forecasted demand."""    return flyte.io.File(path=solve_routes(demand))  @etl_env.taskasync def nightly_plan(lanes: list[str], day: str) -> flyte.io.File:    """Load every lane, forecast in parallel, then solve once."""    # Thousands of concurrent loads — one task per lane    history = await asyncio.gather(        *[load_lane(lane=l, day=day) for l in lanes]    )    demand = await asyncio.gather(        *[forecast(history=h, horizon=14) for h in history]    )    return await optimize(demand=list(demand))

Fan out over every lane

One asyncio.gather becomes thousands of concurrent tasks. ReusePolicy keeps containers warm so short tasks don't pay startup cost.

Reruns skip settled data

cache="auto" hashes inputs and code. A late feed for one lane reruns that lane, not the night.

Right machine per step

Cheap wide workers for ingest, a 64 GiB solver node for optimization. Each task declares its own image and resources.

Typed handoffs, big data by reference

flyte.io.DataFrame moves a pointer to object storage, not the rows. Type mismatches fail before anything runs.

From the Community

Logistics on Union

Case studies, technical deep dives, and conversations with the logistics and travel teams building on Union.

Start today and scale with confidence.

See how logistics and travel teams move billions of records a day with Python-native orchestration.

Try the devbox

A free, local sandbox to
explore the Union.ai platform.

Chat with an engineer