Quick links
Get started
- Where Duckle runs
- What is Duckle?
- What's new in v0.7.0
- What's new in v0.6.1
- What's new in v0.6.0
- Quickstart (60 s)
- Download / Install
- Build from source
- Run your first pipeline
Use the product
- Meet Duckie (AI)
- How to use Duckle
- Recipes / examples
- In-app Git (GitHub/GitLab)
- Workspace + Git flow
- Schedules
- Plans
- Server deployment
- Sign-in and roles
- How a request is decided
- API keys, for machines
- MCP server: connect Claude, Cursor or any agent
- Connection management
- Context variables
Reference
- Capabilities matrix
- Sources
- Transforms
- Sinks
- Data quality
- Custom code
- Control flow
- Advanced settings
- Engines
- Configuration
Resources
- Architecture
- Clean data for AI
- Performance tips
- FAQ
- Troubleshooting
- CI / CD
- Status
- Roadmap
- Contributing
- Sponsor Duckle
- License
- Releases
- Roadmap doc
- Contributing doc
What is Duckle?
An open-source ETL platform you run on your own infrastructure. Drag sources, transforms, validators and sinks onto a canvas, wire them together, and press Run. Duckle compiles the graph to SQL and executes it on a real columnar engine, with live previews, the generated SQL visible on every node, and no hidden state.
You build a pipeline on a laptop and deploy that same file to a server, where it runs on a schedule under a web console with roles, alerts and an audit log. Nothing is rewritten in between, and nothing is metered.
In short: a free, open-source, single-engine alternative to hosted, per-row-priced ETL platforms like Fivetran and Airbyte - one pipeline for ingest, transform, and load that runs anywhere, and can also run dbt on DuckDB inside the same tool.
Three things set it apart:
- An AI assistant that ships in the box. Describe the pipeline you want in English; Duckie writes the JSON and drops it onto the canvas. The model runs locally - no API key, no telemetry, no cloud round-trip.
- 360+ components ready at install time. Files, lakehouses, SQL databases, warehouses, NoSQL, vector DBs, streaming brokers, SaaS REST/GraphQL APIs, even FTP and IMAP - working today, not coming-soon.
- A self-contained binary you can audit. 73 to 110 MB depending on your platform. Engines install on first launch. Workspaces are plain files in a folder you choose. Diff them, branch them, ship them.
Why Duckle is different
| Visual, never opaque | The canvas compiles to SQL you can read, and every node has a live preview tab. No black box. |
| Local-first AI | An assistant that runs on your laptop without an API key. Your prompts, your data, your machine. |
| Single-file binary, no bundled DB | 73 to 110 MB depending on platform (it embeds the headless runner + MCP server). DuckDB downloads on first launch with a guided step. AI engine is opt-in. |
| Native speed | Execution runs through DuckDB: vectorized, columnar, local. A clean-and-export job that crawls in a spreadsheet finishes in milliseconds. |
| Git-friendly by design | Pipelines, connections, contexts, and routines persist as plain files in a folder you pick. Diff them, branch them, review them. |
| 360+ components ready today | Files, databases, warehouses, lakehouses, object stores, SaaS APIs, NoSQL, streaming brokers, vector DBs, FTP, IMAP, SMTP. Each is covered by tests. |
| Honest about scope | Single-machine and embedded by design. Built to make local and small-team data work fast, not to replace a distributed warehouse. |
| 60 UI languages | Topbar, palette, chat assistant, properties panel, and common dialogs ship localized. English, Spanish, Chinese (Simplified + Traditional), Hindi, Arabic, Portuguese (Brazil), Bengali, Russian, Japanese, Punjabi, German, Korean, French, Vietnamese, Telugu, Marathi, Turkish, Tamil, Urdu, Persian, Polish, Italian, Ukrainian, Indonesian, Thai, Dutch, Hebrew, Swedish, Greek, Czech, Hungarian, Romanian, Filipino, Malay, Norwegian, Danish, Finnish, Catalan, Bulgarian, Slovak, Croatian, Serbian, Slovenian, Lithuanian, Latvian, Estonian, Khmer, Burmese, Sinhala, Nepali, Swahili, Afrikaans, Welsh, Irish, Icelandic, Albanian, Azerbaijani, Mongolian, Kazakh. RTL (Arabic, Hebrew, Persian, Urdu) supported. Switch languages from the topbar globe. |
| Open source | Dual-licensed MIT OR Apache-2.0. Yours to use, fork, and extend. |
Screenshots
Real pipelines, built and run in Duckle - not mockups.
Quickstart (60 seconds)
- Download the binary for your OS (see Download / Install above) - or build from source.
- Launch it. First run shows the setup modal:
- Click Install on DuckDB (required, takes ~30 s).
- Optionally click Install on Duckie AI Assistant (~1.1 GB, takes 5-10 min on average broadband).
- Pick a workspace folder. Pipelines, connections, context variables, and routines live there as plain files.
- Build a pipeline two ways:
- Drag + wire: drag a CSV source in, point it at
samples/orders.csv, hit Autodetect schema. Drag a Filter, wire it up. Drag a Parquet sink with an output path. Press Run, watch the nodes light up. - Ask Duckie: click the Sparkles icon (top-right of the toolbar), type "read orders.csv, filter where status = 'paid', write to paid.parquet". When Duckie streams back a pipeline, click Insert into canvas.
- Drag + wire: drag a CSV source in, point it at
- Inspect. Click any node to see its generated SQL in the Plan tab and a live row sample in the Preview tab.
That's a real, native ETL pipeline built and run in under a minute. CSV is just the easiest first node; swap in Parquet, JSON, S3, Snowflake, MongoDB, or Stripe the same way.
Download / Install
Pick the binary for your OS from the latest release:
| OS | Asset | How to run |
|---|---|---|
| Windows | Duckle-windows-x64.exe |
Double-click. Unsigned binary - Windows SmartScreen will warn the first time; click "More info" -> "Run anyway". |
| macOS (Apple Silicon) | Duckle-macos-arm64 |
chmod +x Duckle-macos-arm64 && ./Duckle-macos-arm64. Right-click -> Open the first time to bypass Gatekeeper. |
| Linux (x86_64) | Duckle-linux-x64 |
chmod +x Duckle-linux-x64 && ./Duckle-linux-x64. Requires WebKitGTK 4.1 (libwebkit2gtk-4.1-0 on Debian / Ubuntu). |
The single-file binary above is all you need for Build Pipeline too: the headless runner is embedded into the app at build time, and exporting a pipeline produces ONE self-contained executable (the engine, the DuckDB CLI, any needed extensions, and the resolved pipeline are all inside that one file). Copy that single file to your server and run or schedule it - no separate runner download required.
One command, nothing installed: it scaffolds sample data and a pipeline, compiles it to SQL, runs it on DuckDB, and shows you the rows.
uvx duckle quickstart
Let an agent do it
Paste this into Claude Code, Cursor, or Codex:
Run
uvx duckle quickstartto build my first pipeline and run it
Nothing to install first. The agent fetches Duckle and the DuckDB engine on demand, runs a real pipeline, and shows you the rows.
CLI only (CI, cron, containers)
If you do not want the desktop studio, install just the headless runner. It is about 27 MB rather than 100 MB or more, has no GUI dependency, and is what a build step actually needs.
pip install duckle
That is the whole install. It brings the DuckDB CLI with it (via the duckdb-cli package published by the DuckDB Foundation), so there is nothing else to fetch and it works offline. Wheels ship for Linux, macOS and Windows on x86-64 and arm64.
It also gives you a Python API, where pipelines are built as code and executed by DuckDB rather than by Python:
import duckle
from duckle import col
(duckle.read_csv("orders.csv")
.where(col.amount >= 20)
.derive(total="round(amount * 1.2, 2)")
.write_parquet("out.parquet")
.run())
Python expressions compile to vectorized SQL at plan time, so no rows pass through the interpreter. See the PyPI page for the full API.
The same package provides the duckle command-line runner for CI, cron, and containers - it bundles the headless runner and the MCP server per platform:
pip install duckle # or run ad hoc, no install: uvx duckle --help
Pipelines execute as SQL on the DuckDB CLI, so the runner needs a duckdb on PATH or DUCKLE_DUCKDB_BIN set (pip install duckdb-cli is the quickest route). Validation does not:
duckle validate # compile-check every pipeline under ./pipelines
duckle validate --json # machine-readable, for a CI step
duckle --pipeline my.json # run one
validate opens no source and writes no sink, so it needs no engine, no credentials and no network. Exit codes are stable: 0 clean, 1 a real finding (a pipeline failed or did not compile), 2 the runner could not start (bad usage, unreadable file, missing engine).
The binary is 73 to 110 MB depending on platform (it embeds the headless runner and the bundled MCP server). On first launch you'll be guided through downloading two engines into your app-data directory:
| Engine | Size | Required? | What it powers |
|---|---|---|---|
| DuckDB CLI | ~30 MB + extensions | Yes - cannot run pipelines without it | Every source / transform / sink that runs as SQL |
| Duckie AI Assistant | ~1.1 GB (llama-server + Qwen 2.5 Coder 1.5B GGUF) | Optional | The chat sidebar that generates pipelines from natural language |
App-data location:
- Windows:
%APPDATA%\io.duckle.app\engines\ - macOS:
~/Library/Application Support/io.duckle.app/engines/ - Linux:
~/.config/io.duckle.app/engines/
Delete the engines/ folder if you ever want to force a fresh install.
Run your first pipeline
A worked example using the bundled samples/orders.csv data.
1. Add a source
- Open the Components sidebar (left). Click Sources -> Files -> CSV.
- Drag it onto the canvas.
- In the right-side Properties panel:
- Path: browse to
samples/orders.csv - Click Autodetect schema - the Schema tab fills in column types from the file, the Preview tab shows the first 20 rows.
- Path: browse to
2. Add a transform
- Components -> Transforms -> Rows -> Filter. Drag onto canvas.
- Wire the CSV source's
mainoutput port to the Filter'smaininput. - In Properties:
- Predicate:
status = 'paid'(you can write raw SQL or use the visual builder) - Filter has two output ports:
pass(rows matching) andreject(rows that don't).
- Predicate:
3. Add a sink
- Components -> Sinks -> Files -> Parquet.
- Wire Filter's
passport to the Parquet sink. - Path:
paid_orders.parquet. Write mode:overwrite. Compression:zstd.
4. Run it
- Press Run in the toolbar. Nodes light up in execution order; row counts appear under each.
- Open the Output tab (bottom panel) to see per-stage timing.
- Click any node to inspect generated SQL in Plan + sampled rows in Preview.
5. Iterate
- Add a Group By before the sink to aggregate. Re-run. Sub-second on small data.
- Cancel mid-run with the Stop button - the DuckDB process is killed cleanly.
- Save your work: Cmd/Ctrl-S writes a JSON pipeline file to your workspace folder.
Where Duckle runs
You build a pipeline on your laptop. The server runs that same file. Nothing is rewritten, exported or converted in between.
flowchart LR
D["Duckle Desktop<br/>your machine"] -->|deploy, needs admin| W
B["Console in a browser<br/>your machine"] -->|turn it on, needs operator| W
W["Workspace on your server<br/>a new schedule lands OFF"] --> C["Scheduler<br/>every 15s, takes what is due"]
C --> R["It runs<br/>on that box, unattended"]
R --> O["Run history, logs, metrics,<br/>alerts, and an audit log"]
O -->|you watch it here| B
| How | What you get | |
|---|---|---|
| Server | duckle-runner serve --workspace /srv/pipelines |
Headless web console, cron scheduler, roles, audit log, alerts |
| Docker | Dockerfile.web |
The same console in a container, behind your own ingress |
| CI | duckle-runner --pipeline p.json |
Any runner. Exit codes and NDJSON logs, nothing to install |
| Standalone | Build Pipeline | One self-contained executable. Drop it on a box, run it from cron or systemd |
| Desktop | The app | Author, debug and inspect. Optional, and never required to run anything |
Nothing here depends on a person's machine being switched on:
- Pipelines are plain files in git. Review them in a pull request, roll them back, and let them outlive whoever wrote them. There is no proprietary repository and no exported binary artifact.
- The console has roles and an audit log, so more than one person can operate it and you can see who did what.
- Secrets are not in the pipeline file. They resolve from the environment or an encrypted per-workspace store at run time.
Working recipes for AWS (EC2, ECS, EKS), Azure (VM, Container Apps, AKS) and Google Cloud (Compute Engine, GKE), with manifests and the mistakes worth avoiding, are at duckle.org/deploy. Three things worth knowing before you start:
- On a non-loopback bind with no credential the console starts unclaimed, and for 15 minutes anyone who can reach it can claim it and become its administrator. Pass
--token, setDUCKLE_CONSOLE_TOKEN, or create accounts withduckle-runner console add-userbefore exposing it. An empty value is refused rather than treated as absent, so an unresolved secret fails loudly instead of opening that window. Who can do what, and how one request is decided, is set out under Sign-in and roles. - The scheduler runs in
serve, not in the editor. Start the editor with schedules armed and it now says so rather than leaving you to wonder why nothing fired. GET /healthzneeds no credential and answersok, so a Kubernetes probe or a load balancer can check liveness without holding a token. Every other route is authenticated, so pointing a probe anywhere else reports the pod unhealthy forever.
Deploying a pipeline to a running server
POST /api/deploy lands a pipeline on a server from wherever it was authored, with the schedule it should eventually run on:
curl -X POST https://duckle.internal/api/deploy \
-H "Authorization: Bearer $DUCKLE_TOKEN" \
-d '{"name":"orders-load",
"pipeline": '"$(cat orders-load.json)"',
"schedule":{"intervalMinutes":30}}'
Two things are deliberate. The schedule arrives disabled, so a cadence someone set while testing on a laptop cannot start firing the moment it reaches production; enabling it is a separate call. And deploying needs admin while enabling needs operator, because a deployed pipeline runs shell and SQL on that host: shipping the code and starting it are two acts, and the audit log records both with the name of whoever did them.
API keys, for machines
A person signs in and gets a session. A machine has no browser and nobody to rotate a password, so it gets a key of its own:
duckle-runner console key-add ci-deployer --role admin --expires-days 90
duckle-runner console key-list # role, state, and when each was last used
duckle-runner console key-revoke ci-deployer
A key carries its own role, so a deploy runner can be admin while a metrics scraper is viewer. It is printed once and stored only as a hash, so a lost key is replaced rather than recovered. key-list shows when each was last used, which is the question actually worth answering before revoking one, and revoking takes effect immediately on a console that is already running rather than at the next restart. Revoked keys are marked rather than deleted, so a key that turns up in an old log can still be named.
Accounts, sessions and keys live in .duckle/console.db. An existing console-users.json is carried into it on first start and renamed to .migrated, so an upgrade neither locks anyone out nor destroys the only copy of a credential store. For the roles, and a diagram of how one request is decided, see Sign-in and roles.
Scaling it
duckle-runner serve is an ordinary service. Run it on EC2, EKS, a VM or a container next to everything else you operate, and scale it the way you scale any service:
- More cores. The engine is parallel and uses every core on the box by default, so a bigger instance is a faster pipeline with no change to the pipeline. Bound it with
DUCKLE_THREADSwhen you would rather it did not take the whole machine. - More RAM. Set
memoryLimitMbper stage, or a workspace default, and spill to disk past it. - More pipelines at once.
DUCKLE_MAX_CONCURRENT_RUNSraises how many run together; it ships at 1 so an unattended server stays predictable until you decide otherwise. - More machines.
duckle-runner workdrains a queued batch from as many workers as you start, on as many hosts as you like, each claiming its items under a lock so nothing runs twice. - Bigger than any box. Turn on pushdown and the query runs verbatim inside Postgres, Oracle, SQL Server or Snowflake. The warehouse does the scan; Duckle keeps the scheduling, lineage, data quality and alerting.
Measured, rather than asserted:
- 96,000,000 rows out of live Postgres to Parquet in 39.9s (details)
- Oracle extract at 65.0s, against 68.6s for python-oracledb with pyarrow on the same machine (details)
The one thing Duckle does not do is split a single query across a cluster the way a distributed warehouse does. When you need that, push the work down into the system that has it and let Duckle orchestrate around it.
Server deployment (Build Pipeline)
Want the studio to publish straight to a running server instead? That is the other route: connect a server once, then Deploy to a server from the editor. Step by step in docs/current/server-deployment.md.
Promoting from CI instead, or driving Duckle from Airflow, Dagster or Temporal? See docs/current/ci-and-orchestration.md, with copyable GitHub Actions and GitLab CI templates in docs/ci/.
Want to know exactly what crosses the wire, and where every credential is stored? docs/current/client-server-architecture.md is the diagrammed answer, sharp edges included.
The in-app scheduler runs only while Duckle is open. To run a pipeline on a server with no desktop app, Build Pipeline turns it into ONE self-contained executable - the equivalent of a standalone "Job".
Right-click a pipeline (in the project tree or on the canvas) and choose Build Pipeline. The output is a single file named after the pipeline (orders_etl.exe on Windows, orders_etl on macOS / Linux) that embeds everything it needs:
- the headless execution engine,
- the DuckDB CLI,
- only the DuckDB extensions that pipeline's components actually use,
- the resolved pipeline (context variables substituted, routines inlined),
- its secrets (see below).
On first run it self-extracts to a temp cache and uses its own embedded DuckDB, so the server needs nothing installed - no Duckle, no DuckDB. There is no folder to copy, no run.sh, and no separate runner download. A CSV-to-CSV pipeline builds to about 28 MB; only the extensions a pipeline uses are bundled, so the file stays lean.
./orders_etl # or orders_etl.exe on Windows
The process exits 0 on success and non-zero on failure, and writes the same NDJSON run logs under logs/ (Splunk / Dynatrace friendly).
Build options
| Option | What it does |
|---|---|
| Target OS | Pick Windows, Linux, or macOS in the build dialog. The native OS always builds; a Linux server file can be cross-built from any host (the Linux engine is bundled for you), while a macOS file can only be produced on a Mac. Appending the payload makes the file unsigned, so do not codesign / Authenticode-sign it. |
| Context | Pick a context at build time; its non-secret variables are baked into the pipeline. |
| Secrets: Environment | Each secret becomes a ${ENV:KEY} placeholder, so nothing sensitive is written into the file. The runner resolves real environment variables first, then a secrets.env (KEY=VALUE lines) placed next to the file. |
| Secrets: Passphrase | Secrets are encrypted inside the file with AES-256-GCM, decrypted at run time from the DUCKLE_BUNDLE_PASSPHRASE environment variable. |
Schedule it with whatever the server already has - point the OS scheduler straight at the file:
# Linux cron - run every day at 02:00
0 2 * * * /opt/duckle/orders_etl >> /var/log/orders_etl.log 2>&1
On Windows use Task Scheduler; on macOS a launchd plist; on Linux a systemd timer. Full examples in docs/current/scheduler.md.
Run against an existing workspace - the same embedded headless runner can also execute a pipeline JSON directly, resolving context the way the app does:
duckle-runner --pipeline /path/to/pipeline.json [--workspace /path/to/workspace] [--duckdb /path/to/duckdb]
Web panel (remote management console)
To run and monitor pipelines on a server with a browser instead of the desktop app, start the built-in web panel - it is part of the same duckle-runner binary, so there is nothing extra to install:
duckle-runner serve --port 8080 --workspace /path/to/workspace
Open http://localhost:8080. The panel has eight views:
- Overview - every pipeline with its last status, duration and next scheduled run, and a Run button.
- Runs - run history across every pipeline (status, duration, rows, errors) with expandable per-pipeline run logs and optional auto-refresh.
- Schedules - an editable cron or interval schedule per pipeline, showing what is running now and what is due next.
- Plans - several pipelines in the order you chose. See Plans.
- Catalog - everything the workspace reads and writes, who owns it, and what is written but never read. See Workspace catalog.
- Batches - work queued for workers: progress, what is running now, what failed, and a retry for the failures.
- People - the accounts that may sign in and the keys machines use, with the role each one has. Admin only.
- Audit - who signed in, what they changed and who was turned away. Admin only, and shown only to admins.
Runs execute in-process through the same engine, are written to the same run history (<workspace>/runs/) and logs (<workspace>/logs/), and a built-in scheduler triggers any pipeline whose schedule has elapsed - so the server itself runs your schedules, no OS cron needed.
Plans: several pipelines, in an order you chose
A schedule runs one pipeline. A plan runs several, in steps: everything inside a step goes at once, and the next step waits for it. A step that fails stops the ones after it, so nothing runs against data that was never produced.
That is the shape most nightly loads already have. Without it they get written as three schedules set a few minutes apart and hoped over, which works until the extract takes four minutes instead of two.
Build one wherever you are: the Plans tab in the web console, or the Plans tile under Operate in the desktop app. Add a step, put pipelines in it, and the card draws the chain it will run.
EXTRACT PUBLISH
orders.json --> export.json
customers.json
Two things worth knowing:
- Every pipeline keeps its own run history. A plan does not collapse into one opaque run, because at three in the morning the question is which step broke, not that the nightly load did.
- A plan can be scheduled like anything else, from its own card. The same plan runs whether the schedule is fired by
serveon your server or by the desktop app on a shared workspace - both readplans.jsonandschedules.json, and both decide it the same way. - It is one file, so it travels. A plan written in the desktop app opens in the console and the other way round, and it goes to your server with everything else in the workspace.
Plans live in <workspace>/plans.json, so they are a file in git alongside the pipelines they order.
Sign-in and roles
Start from where you actually are.
Running it on your own machine? Nothing to do. On 127.0.0.1 with no accounts the console is open, because anyone who can reach it is already sitting at the machine, and asking them for a password would protect against an attacker who has already won.
Put it on a server and it refused to start? That is the feature. The console can run any pipeline in the workspace, and a pipeline can run shell and SQL, so reaching it is the same as running code on that host. A bind it cannot authenticate fails rather than serving anyone and printing a warning nobody reads. The shortest way past it:
DUCKLE_CONSOLE_TOKEN=<secret> duckle-runner serve --host 0.0.0.0 --port 8080
More than one person? Give each of them their own, so the audit log can name them. The token is printed once and kept only as an Argon2id hash:
duckle-runner console add-user reporting --role viewer
duckle-runner console add-user ops --role operator
duckle-runner console list
A machine needs in? CI, a scraper, or your own laptop deploying: those have no browser and nobody to rotate a password, so they get a key instead of an account. See API keys.
| Role | Can |
|---|---|
viewer |
Read the dashboard, run history, logs, schedules and catalog. |
operator |
Everything a viewer can, plus run pipelines and change schedules. |
admin |
Everything an operator can, plus deploy pipelines, connections, credentials, the audit log and the workspace itself. |
The split follows what an action can destroy, not which screen it lives on. It is why deploying a pipeline needs admin while turning its schedule on needs operator: shipping code to a host and deciding when trusted code runs are different sizes of decision.
How a request is decided
Three ways to prove who you are, one identity, one check, and every outcome recorded:
flowchart LR
R([Request]) --> C{"Session cookie?"}
C -->|within 12h| ID["Identity<br/>name + role"]
C -->|no| B{"Bearer token?"}
B -->|API key| ID
B -->|account token| ID
B -->|nothing| U["401<br/>sign in"]
ID --> P{"Role enough<br/>for this route?"}
P -->|yes| OK["It happens"]
P -->|no| F["403<br/>refused"]
OK --> A[("audit log<br/>who, what, when")]
F --> A
U --> A
Two things follow from that shape. A refusal is recorded as carefully as a success, so audit --outcome denied answers "who is reaching for what they do not have". And a route with no entry in the permission table needs admin, so a route added later is locked down rather than accidentally left open.
A browser trades its credential for a session cookie, so it never stores the credential itself: the cookie carries a random session id, is HttpOnly and SameSite=Strict, is marked Secure when a proxy tells Duckle the browser is on https, and lasts 12 hours. Sessions survive a restart, so a rolling deploy does not sign your team out.
Accounts, sessions and keys live in <workspace>/.duckle/console.db. Nothing in it can be replayed: an account token is an Argon2id hash, and a session id and an API key are both generated with 256 bits of entropy and stored as SHA-256, so a copy of the file or a backup of the workspace admits nobody. An older console-users.json is carried in on first start and renamed .migrated, so upgrading neither locks anyone out nor destroys the only copy of a credential store. The same accounts, roles and keys cover duckle-runner web.
Read it back from the Audit view, or from a terminal with no server running:
duckle-runner audit # newest first, 50 by default
duckle-runner audit --outcome denied # who reached for what they do not have
duckle-runner audit --actor ops --action schedule # one person, one family of actions
duckle-runner audit --limit 500 --json # for a collector
allowed means the caller was permitted to proceed, not that the work then succeeded - run history answers that. Reads are not recorded, so a dashboard polling every few seconds does not bury the entries worth seeing. A page says when older entries exist beyond it, and a line that will not parse is counted rather than silently skipped.
Still put it behind a reverse proxy if you need TLS.
Migrating a repository of legacy jobs
The editor imports one job at a time, which is how you try Duckle. This is how you leave another tool: point it at a checkout and convert everything.
duckle-runner import ./legacy-jobs # convert the tree into ./imported
duckle-runner import ./legacy-jobs --out ./pipelines # somewhere else
duckle-runner import ./legacy-jobs --json # for a migration script
duckle-runner import ./legacy-jobs --strict # CI gate, exits 1 if anything needs a person
The folder layout is mirrored rather than flattened, because two jobs in different folders routinely share a name and flattening would silently drop one. Files that are not jobs - routines, contexts, SQL templates - are reported separately rather than counted as conversions, and no empty pipeline is written for them.
Reusable job bodies convert alongside jobs, and a job's children resolve to the files they became, so the master/child/joblet graph survives the move rather than arriving as a set of disconnected pipelines. A loop or iterate body is lifted into its own pipeline that the parent calls, which is why the file count comes out higher than the job count.
The closing tally is the number to decide on. It says how many jobs came across clean, how many need a person, and which components have no equivalent yet, sorted by how often they appear. On a real 125-file corpus that list had a single entry, a site-specific custom component: coverage is the head of the distribution, so a corpus usually converts far better than a raw component count suggests.
What remains is credentials and Java. Credentials were never in the job files: encrypted
passwords become ${ENV:...} placeholders and connections defined outside the job are
named so you can point them at a saved connection. Java is the part that needs a person,
and the report separates it so you can see how much there is:
- A mapper expression is translated when it has one faithful SQL reading: a literal, a cast, a character function, a comparison, a choice, arithmetic. Anything whose meaning depends on a Java type the job file does not record stays reported, because guessing there produces a silently wrong number rather than a failure.
- A Java body is never turned into something that compiles. It imports with no SQL and fails, since a pipeline that runs while omitting the rules is worse than one that stops. A body whose every statement is a print carries no rules and is called out separately, so a long list sorts into what can be deleted and what has to be ported.
A component with no equivalent is imported as a named placeholder and reported. That includes a job body's input and output ports: a child pipeline runs for its side effects, so it does not yet take rows from its caller or hand them back.
How a write writes is carried across. A warehouse sink records whether it appends rows or amends the ones already there, and importing that as the default write mode turned an append into a full-table replace - so on a table several nodes write
No comments yet
Be the first to share your take.