diff --git a/.claude/launch.json b/.claude/launch.json
index 156aab530..44f818965 100644
--- a/.claude/launch.json
+++ b/.claude/launch.json
@@ -6,6 +6,12 @@
"runtimeExecutable": "npm",
"runtimeArgs": ["run", "dev", "--prefix", "flowfile_wasm"],
"port": 5174
+ },
+ {
+ "name": "docs-site",
+ "runtimeExecutable": "python3",
+ "runtimeArgs": ["-m", "http.server", "8765", "--directory", "site"],
+ "port": 8765
}
]
}
diff --git a/.claude/skills/flowfile-docs-and-writing/SKILL.md b/.claude/skills/flowfile-docs-and-writing/SKILL.md
index 317e7eb8b..e30f6f89b 100644
--- a/.claude/skills/flowfile-docs-and-writing/SKILL.md
+++ b/.claude/skills/flowfile-docs-and-writing/SKILL.md
@@ -7,6 +7,7 @@ description: How the Flowfile docs site (MkDocs + Material, the docs/ tree, mkdo
## When NOT to use this skill
+- Editorial standard for docs pages (house style/voice rules, the persona nav map, fact-checking a docs claim against code, the tested-examples contract and add-an-example recipe) → `flowfile-docs-review` (this skill owns *how the site builds*; that one owns *what good pages say and how to verify it*).
- CI gates that happen to touch docs (`check-formula-docs`, `check-stubs` jobs, version-sync) and release mechanics → `flowfile-change-control` (this skill explains *what* the formula-docs generator does and the doc-authoring traps; that skill owns *why the CI job exists and what blocks a merge*).
- flowfile_frame public API design, `.pyi` stub authoring, expression codegen → `flowfile-frame-and-codegen`.
- Writing/reviewing a custom node's `NodeSettings` docstrings for the Node Designer UI → `flowfile-node-development`.
@@ -23,7 +24,7 @@ Two documentation surfaces exist; don't confuse them:
| Surface | Source | Audience |
|---|---|---|
-| **MkDocs site** (`docs/`) | ~64 Markdown pages + `docs/index.html`, built by `mkdocs.yml`, deployed to `https://edwardvaneechoud.github.io/Flowfile/` | End users (visual editor, Python API) and external contributors |
+| **MkDocs site** (`docs/`) | Markdown pages + `docs/index.html` (count them: `find docs -name '*.md' | wc -l`), built by `mkdocs.yml`, deployed to `https://edwardvaneechoud.github.io/Flowfile/` | End users (visual editor, Python API) and external contributors |
| **CLAUDE.md files** (9: root + 8 packages) | Not part of the MkDocs build; plain files read by AI agents and human contributors | Anyone working *inside* the repo — see §6 |
The `docs/` tree structure mirrors `mkdocs.yml`'s `nav:` block almost exactly (verify by diffing them — see §3.3 for the one place they currently disagree). Top-level layout:
@@ -74,14 +75,7 @@ Every new page must be added to `mkdocs.yml`'s `nav:` block by hand — MkDocs d
### 3.3 Forgetting the `nav:` entry makes a page invisible, not broken
-MkDocs builds every `.md` file under `docs/` whether or not it's listed in `mkdocs.yml`'s `nav:` — a missing nav entry does **not** fail the build (§2's "no `--strict`" gotcha) and does **not** stop the page from existing at its URL if something else links to it directly. It just means the page is absent from the sidebar and site search weighting. This is not hypothetical: as of 2026-07-03, `docs/for-developers/docker-deployment.md` (the doc covering Docker deployment and Group-Based Sharing for multi-user mode) exists on disk, is linked *to* from `users/visual-editor/catalog/secrets.md` (the only real inbound page link — `users/deployment/docker.md` merely references a similarly-named image asset directory), but is **not in `mkdocs.yml`'s `nav:` at all**. Running `poetry run mkdocs build` prints exactly:
-
-```
-INFO - The following pages exist in the docs directory, but are not included in the "nav" configuration:
- - for-developers/docker-deployment.md
-```
-
-and still exits 0. If you add a new page and it doesn't show up in the sidebar after a build, this is almost certainly why — check `mkdocs.yml`'s `nav:` block before assuming something is actually broken.
+MkDocs builds every `.md` file under `docs/` whether or not it's listed in `mkdocs.yml`'s `nav:` — a missing nav entry does **not** fail the build (§2's "no `--strict`" gotcha) and does **not** stop the page from existing at its URL if something else links to it directly. It just means the page is absent from the sidebar and site search weighting. The build prints an `INFO` line ("The following pages exist in the docs directory, but are not included in the 'nav' configuration") and still exits 0. If you add a new page and it doesn't show up in the sidebar after a build, this is almost certainly why — check `mkdocs.yml`'s `nav:` block before assuming something is actually broken. (The long-standing live example, an orphaned `for-developers/docker-deployment.md`, was merged into `users/deployment/docker.md` in the 2026-07 docs rebuild, with a `redirects`-plugin mapping preserving its URL — as of that rebuild the nav is orphan-free, and the redirect map in `mkdocs.yml` is where moved pages get recorded.)
### 3.4 The formula reference is generated — never hand-edit it
@@ -102,12 +96,7 @@ If you bump the `polars-expr-transformer` version pin (or edit `tools/generate_f
## 4. House style for docs pages
-Two registers coexist in `docs/`; know which one you're extending:
-
-1. **Legacy/marketing register** — `quickstart.md`, `users/index.md`, `nodes/index.md`, most `python-api/` pages, `design-philosophy.md`: first-person-plural, emoji section headers (🎨 🐍 ✅ 🚀), heavy inline-styled HTML (`clamp()` font sizing, gradient `
` cards), `` collapsibles, hype prose.
-2. **Modern/engineering register** — `ai/index.md`, `deployment/lite.md` + `deployment/index.md`, `kernel-architecture.md`, `ai-architecture.md`, `visualizations.md`, `docker-deployment.md`'s sharing section, `community.md`: tight declarative prose, Material admonitions (`!!! info`, `!!! tip`, `!!! warning`, with a quoted title), comparison tables, exact file paths and symbol names, explicit invariant statements ("There is no min/max gate and nothing that rejects…"), mermaid diagrams where a picture beats prose.
-
-**Write new pages in register 2.** Recent doc commits (LSP notebook docs, project-tracking docs, node-reference docs, formula-docs regeneration) all trend toward register 2, and it matches the voice of `CONTRIBUTING.md` and every `CLAUDE.md` file. Register 1 pages are legacy, not a style to imitate going forward.
+Since the 2026-07 docs rebuild there is **one register**: tight declarative prose, Material admonitions (`!!! info`, `!!! tip`, `!!! warning`, with a quoted title), comparison tables, exact file paths and symbol names, explicit invariant statements, mermaid diagrams where a picture beats prose. The old marketing register (emoji headers, gradient `
` cards, `clamp()` inline CSS, hype prose) was eliminated page-by-page — do not reintroduce it. The full editorial standard — voice rules (including the ban on generic "Tips for Success" filler), the persona nav map, the claim→source verification index, and the tested-examples contract — lives in `flowfile-docs-review`; read that skill before writing or reviewing any page.
Admonition syntax (register 2's signature move):
@@ -185,9 +174,7 @@ Gaps worth knowing so you don't assume something is documented when it isn't, an
- **No PR template, no feature-request issue template.** The only issue template (`.github/ISSUE_TEMPLATE/bug_report.md`) is GitHub's unmodified default — it still asks for "Smartphone… Device: [e.g. iPhone6]," which makes no sense for a desktop/server ETL tool. `CONTRIBUTING.md`'s prose ("what changed, why, how you tested it; screenshots for UI changes") is the only PR-description guidance that exists.
- **No `SECURITY.md`** at the repo root or in `.github/`. The private-vulnerability-reporting policy (GitHub's private "Report a vulnerability" form) is documented only inside `CONTRIBUTING.md`'s "Reporting security issues" section.
- **`claude.yml` and `claude-pr-review.yml`** (interactive `@claude`-mention agent, and an automatic Claude code review posted on every non-draft PR) are real, tracked workflow files in `.github/workflows/` but are not mentioned in any docs page or in `CONTRIBUTING.md`'s workflow list — a first-time contributor gets an automated review with no explanation of where it came from.
-- **Flow-in-flow** (subflows — PR #568, merged) has no docs page anywhere under `docs/` as of 2026-07-03 (a `docs/users/python-api/tutorials/flowfile_frame_api.md` hit for "Flow in Flow" is a false positive — it's a `` caption unrelated to the feature).
-- **Project git-tracking** (`flowfile project init|open|save`, `FLOWFILE_ENABLE_PROJECTS`) gets only a passing mention in `docs/users/deployment/docker.md`; there's no dedicated user guide despite it being a full feature (#524).
-- **Group-based sharing** (multi-user resource sharing) is documented for developers in the orphaned `for-developers/docker-deployment.md` (§3.3) but has no end-user guide page.
+- ~~Flow-in-flow, project git-tracking, group-based sharing undocumented~~ — closed by the 2026-07 rebuild: `users/visual-editor/subflows.md`, `users/projects.md`, `users/deployment/sharing.md`, plus `users/deployment/cli.md` (headless runs), `users/connect/{index,kafka,apis}.md`, and `users/coming-from-excel.md` now exist and are in nav.
If you're asked to fill one of these gaps, write it in register 2 (§4), put user-facing content under `users/` and internals under `for-developers/`, and don't forget the `nav:` entry (§3.3).
@@ -200,7 +187,7 @@ All facts above were spot-verified in this repo on 2026-07-03 against app versio
```bash
# Docs site config and orphan-page check (reproduces the exact CI build)
poetry run mkdocs build 2>&1 | grep -E "not included in the|^ERROR"
-# expect: "for-developers/docker-deployment.md" flagged as not in nav, and no line starting "ERROR"
+# expect: no orphan pages and no line starting "ERROR" (orphans were eliminated in the 2026-07 rebuild)
# Confirm the formula reference is still generated (not hand-edited) and current
head -1 docs/users/formulas/functions.md # expect the AUTO-GENERATED header
diff --git a/.claude/skills/flowfile-docs-review/SKILL.md b/.claude/skills/flowfile-docs-review/SKILL.md
new file mode 100644
index 000000000..4482c0940
--- /dev/null
+++ b/.claude/skills/flowfile-docs-review/SKILL.md
@@ -0,0 +1,150 @@
+---
+name: flowfile-docs-review
+description: The editorial standard for Flowfile's docs site — the house style (register-2 voice rules), the persona-based nav map (which tab serves which arriving audience), the claim-type→source-of-truth verification index for fact-checking any docs statement against code, and the zero-drift examples contract (how tested .py and .yaml examples are structured, included via snippets, and auto-tested). Use when writing or reviewing any page under docs/, fact-checking a docs claim (node counts, Lite availability, API signatures, ports, providers, defaults), adding a worked example or tutorial, deciding where a new page belongs in the persona nav, or running an editorial/lint pass over docs changes.
+---
+
+# Flowfile docs review — style, personas, verification, examples
+
+## When NOT to use this skill
+
+- Site build mechanics, `mkdocs.yml` traps (`use_directory_urls`, nav invisibility, the raw-HTML home page), deploy pipeline, formula-docs regeneration, comment doctrine for source code, CLAUDE.md maintenance → `flowfile-docs-and-writing` (this skill owns *what good pages say and how to verify it*; that one owns *how the site is built*).
+- CI gates and release mechanics → `flowfile-change-control`.
+- Per-package test commands and Docker fixture mechanics beyond the examples contract → `flowfile-testing-and-validation`.
+
+## 1. House style (register 2 — the only register for new/edited pages)
+
+The canonical exemplar is `docs/ai/index.md`. Rules, all checkable:
+
+1. **Declarative present tense, second person where needed.** No first-person-plural marketing ("we've got you covered"), no hype adjectives (powerful, seamless, blazing, incredible, professional-grade), no exclamation-mark enthusiasm, no unverifiable stats ("thousands of users").
+2. **No emoji** in headings, bullets, or feature lists. No `## **Bold-in-heading**` markdown.
+3. **No inline-styled HTML blocks** in `.md` pages (gradient divs, `clamp()` fonts, `style=` attributes). Visual variety is wanted — plain-markdown-only pages read as flat — but it comes exclusively from the shared brand layer: Material **grid cards** (`
`, brand accent bar styled globally in `docs/stylesheets/extra.css`), **content tabs** (`=== "Label"` — use for install methods and platform variants), **icon shortcodes** (`:material-*:` / `:octicons-*:` via `pymdownx.emoji`), the `ff-paths`/`ff-path-teal`/`ff-path-purple` chooser classes from `extra.css`, admonitions, and mermaid. New visual components get a class in `extra.css` (both color schemes), never a `style=` attribute. Section indexes and audience routers should use cards; high-traffic pages (quickstart, guides-by-audience) carry the most visual weight, deep reference pages the least.
+4. **No "coming soon" / roadmap promises.** Document what ships. Aspirational notes only as a short "Future direction" admonition on developer pages, clearly labeled.
+5. **Material admonitions** (`!!! note "Quoted Title"`, `tip`, `warning`, `info`) for asides; roughly ≤2 per screenful.
+6. **Exact names everywhere**: UI labels as the component renders them (Group By's average is **Mean**, `GroupBy.vue`), API symbols as actually exported, file paths and env vars in backticks. When docs and UI disagree, the Vue component is the truth.
+7. **Numbers rot — prefer pointers.** "See `configs/node_store/nodes.py`" beats "46 nodes". A literal count needs an entry in §3's index and, if fast-rotting (versions, model names, node counts), a date stamp ("as of 2026-07").
+8. **Runnable code is never hand-written in a page.** Any Python block presented as runnable is a `--8<--` include from `docs/examples/` (§4). Inline fragments are allowed only for formulas, UI config values, or shell one-liners — with verified syntax.
+9. **Every page opens with one orientation paragraph**: who it's for, what they'll be able to do after reading. Then short sections; tables for enumerable facts with the explanation in surrounding prose.
+10. **Links**: relative `.md` links between pages (MkDocs rewrites them); raw `.html`/directory forms only inside `docs/index.html`. Every new page gets a `nav:` entry; moved/merged URLs get a `redirects` plugin mapping in `mkdocs.yml`.
+11. **Images**: never create or edit image files; never block on one. Reuse an existing `docs/assets/images/` asset if apt, else leave `` at the spot. In step-by-step walkthroughs, per-step screenshots go in fold-outs so they don't break the reading flow (maintainer-preferred pattern): `See it: …` wrapping the image plus its placeholder/refresh comment.
+12. **Cross-page consistency beats local polish**: one canonical statement per fact, siblings link to it (the join-type list, the Lite node inventory, and the custom-node `process()` signature have each previously diverged across three pages).
+13. **No filler advice — maintainer-rejected pattern.** Generic "Tips for Success" bullet lists ("Start simple", "Save regularly", "Preview often", "Use descriptions", "Try both modes") are banned. If a tip isn't specific to the page's subject and non-obvious, cut it. Never state the obvious ("click Run to run the flow").
+14. **Length is a quality dimension.** Each page earns its length: getting-started/how-to pages are short concrete numbered steps; reference pages are complete tables; concept pages explain once and stop. If a section restates what an adjacent section or page already says, cut or link.
+15. **Model division of labor**: subagent drafts are raw material. The final shipped text of user-facing pages is written/edited by the lead (Fable) after a dedicated scan for filler, obviousness, length, and cross-page consistency.
+
+## 2. Persona nav map (target IA)
+
+One tab per arriving audience; the Home page routes with persona cards (one hop).
+
+**Persona cards route to persona landing pages, never to feature pages** (maintainer-corrected, 2026-07). Six persona routes live under Get Started → By Audience: `coming-from-excel`, `build-flows-visually`, `analyze-your-data`, `data-elsewhere`, `write-python`, `deploy-for-a-team` (all in `docs/users/`). The bar a persona page must meet (maintainer-refined twice — narrow personas and feature-list cadence were both rejected):
+
+- **Broad opening**: describe the *situation* through several recognizable faces (roles, jobs-to-be-done), not one caricature — "finance person reconciling exports, marketer sizing a campaign, researcher cleaning waves…" beats "your job is answers about cities".
+- **A stated mental model near the top** (one or two sentences: how Flowfile thinks about this person's problem) plus an `IMAGE-PLACEHOLDER-TO-CHANGE` describing a mental-model diagram for the maintainer to draw.
+- **Depth per stop, not feature lists**: each numbered stop = the person's problem at that point → how the product's model answers it (the *why*) → a concrete example → the deep link. A stop that only names features and links is below the bar.
+- **A real example on every page**: tested `--8<--` includes for runnable Python, verified formula/shell fragments otherwise.
+- **Feature pages are linked stops** — the catalog is a stop on the analyst's route, not the destination. A persona page summarizes, it never re-teaches.
+- **No-code truth**: every route that works without code says so explicitly; never let a `ff.*` column imply Python is required.
+- **Shared shape**: numbered stops, `---` rule, one-line `**Fastest first taste:**` footer. If a card's target is a feature index, either the card is mislabeled or the persona page is missing.
+
+Above the personas sits the **identity tree** (Get Started → What is Flowfile): a short **root** `docs/what-is-flowfile.md` that does positioning only — what space the product occupies (the gap after spreadsheets stop scaling, before a data-engineering team is the only option; four bundled parts; reproducible by construction) — and forks via lens cards into two tagged branches: `what-is-flowfile-plain.md` (non-technical: reproducibility as the felt promise, catalog as where it compounds) and `what-is-flowfile-technical.md` (engineers: **absence of glue** — secrets, Python envs with the right rights, database/cloud/Kafka connection plumbing, keeping data current without an orchestrator, data organization, visualization, explaining your work). Root stays short; depth lives in the branches; branches close on the same reproducible-by-construction line. **Register split between the lenses (maintainer-directed)**: the plain branch may use warm, figurative prose (recipes, kitchens — it's the one page where that's allowed); the technical branch is flat and declarative — every claim states its mechanism, no metaphors, headers like "What it automates", not "The annoyances it eats". Technical readers parse figurative language as padding. Persona pages answer "how do I, given who I am"; the identity tree answers "what is it" — don't blur them, and keep the root-and-branch cross-tags intact.
+
+| Tab | Arriving reader | Owns |
+|---|---|---|
+| Home (`index.html`) | everyone | value prop, sales-pipeline showcase (tested flow download + live-demo deep link), persona router |
+| Get Started | new user, any kind | the What-is identity tree, `installation.md` (**the canonical install home** — all five paths as tabs; quickstart carries only the pip fast-path and links here; deployment pages hold per-edition depth), `quickstart.md` (first visual flow + first Python pipeline), Flowfile Lite, and the six By-Audience persona routes |
+| Visual Editor | analysts building flows | overview, building flows, formulas (+ generated function reference), node reference (6 categories), kernels, node designer, worked examples |
+| Connect Your Data | "my data lives elsewhere" | connector matrix, connections & secrets, databases, cloud storage (S3/ADLS/GCS), Kafka, REST APIs & Google Analytics |
+| Catalog & Automation | analyzing/operating data | catalog, virtual tables, SQL editor, visualizations, schedules, subflows, projects & git |
+| Python API | Python developers | quickstart, concepts, reference, tutorials (all examples tested) |
+| AI Assistant | any | feature catalog, provider setup (BYOK) |
+| Deploy & Operate | admins | desktop, pip, Docker (single merged page), users/groups/sharing, headless runs & CLI |
+| For Developers | contributors | architecture, internals, kernel, AI architecture, custom nodes |
+
+The **analysis** journey (analyze data without building pipelines) is deliberately multi-hooked: Home persona card → Catalog tab; quickstart's visual track ends in Catalog Writer → SQL editor → visualization; `flowfile seed-demo` documented as the one-command populated catalog.
+
+## 3. Claim-type → source-of-truth index
+
+Verify against code, never against another prose doc (READMEs and CLAUDE.md drift too). The most drift-prone claim types and where each is decided:
+
+| Claim about | Source of truth |
+|---|---|
+| Core node types, categories, laziness, narrow/wide | `flowfile_core/flowfile_core/configs/node_store/nodes.py` (`get_all_standard_nodes`; plus dict-only `polars_lazy_frame`) |
+| Lite/WASM node availability | `flowfile_wasm/src/config/nodeDescriptions.ts` + `flowfile_wasm/src/components/Canvas.vue` `nodeCategories` (`available: false` flags). 23 usable nodes as of 2026-07 — historic "18" was wrong |
+| Join strategies, fuzzy algorithms, group-by agg options | `flowfile_core/.../schemas/transform_schema.py` (`JoinKeyStrategy` = inner/left/right/full/semi/anti/outer; `FuzzyTypeLiteral` = 6 algorithms); UI labels in `GroupBy.vue` (`mean` → "Mean") |
+| Node settings fields, file formats, write modes | `flowfile_core/flowfile_core/schemas/input_schema.py`; cloud: `cloud_storage_schemas.py` (`CloudStorageType` s3/adls/gcs; `AuthMethod` — the CLI literal is `aws-cli`, hyphen; read formats include `iceberg`) |
+| `ff.*` availability | `flowfile/flowfile/__init__.py` — NOT the same as `flowfile_frame/flowfile_frame/__init__.py` (e.g. `read_ipc`/`read_ndjson`/`read_avro` exist in frame but are not re-exported as `ff.*`) |
+| Expression methods | `flowfile_frame/flowfile_frame/expr.py` + pinned Polars (renames: `cum_sum` not `cumsum`, `weekday` not `day_of_week`; no `FlowFrame.drop_duplicates`/`vstack`/`__len__`) |
+| AI providers and models | `flowfile_core/flowfile_core/ai/providers/registry.py` (6 BYOK + local pseudo-provider); per-provider `default_model` vs per-surface models differ — Groq default is `qwen/qwen3-32b` |
+| CLI verbs and flags | `flowfile/flowfile/__main__.py` (run ui/core/worker/flow, seed-demo, remove-demo, project init/open/save) |
+| Ports | `flowfile/flowfile/api.py`, `shared/storage_config.py`; the web UI is hard-locked to 63578 (`flowfile/web/__init__.py` raises on any other port — `FLOWFILE_PORT` does not move it) |
+| Health probes | `flowfile_core/routes/public.py` — `/health/status`; there is no `/health` on core or worker |
+| Kernel images and defaults | `flowfile_core/flowfile_core/kernel/manager.py` (image tags), `kernel/models.py` (default memory 4 GB, CPU 2) |
+| Password/auth policy | `flowfile_core/auth/password.py` (8 chars + number + special; no case rules) |
+| Storage paths per mode | `shared/storage_config.py` + `docker-compose.yml` overrides (`FLOWFILE_USER_DATA_DIR=/app/user_data` in shipped compose) |
+| Docker deployment facts | `docker-compose.yml` itself (volume `flowfile-internal-storage`, `shm_size`, scheduler enabled in shipped compose, `FLOWFILE_INTERNAL_TOKEN` required). The bundled compose **builds from source**; server/HTTPS deployments are the separate [flowfile-hosting](https://github.com/Edwardvaneechoud/flowfile-hosting) kit (published images pinned via `FLOWFILE_VERSION`, Caddy/Cloudflare-Tunnel/LAN ingress, `./install.sh`) — verify hosting claims against that repo, and make hosting *changes* there, never in this repo |
+| Catalog internals | `flowfile_core/flowfile_core/catalog/` (services/, `constants.py` — thumbnail cap 500 KB, SQL recursion limit 5) |
+| Flow save format | `.yaml` default (`.yml`/`.json` accepted; `.flowfile` is legacy pickle, open-only) — `flowfile/manage/io_flowfile.py` |
+| Formula functions | generated `docs/users/formulas/functions.md` (never hand-edit; `make formula_docs`) |
+| App version | root `pyproject.toml` only — never hardcode in prose |
+
+## 4. Zero-drift examples contract
+
+Two example kinds, two automatic gates each (pytest at runtime, `pymdownx.snippets check_paths: true` at build time).
+
+**Visual flow examples** — committed at `data/templates/flows/.yaml`:
+- Carry `_template_meta` (template_id, name, category Beginner/Intermediate/Advanced, tags, node_count, icon) and `_required_csv_files`; read nodes use `__TEMPLATE_DATA_DIR__/.csv`.
+- Auto-tested with zero new code: `flowfile_core/tests/templates/test_template_flows.py` globs the directory, validates, substitutes the placeholder, opens via `open_flow`, runs with `execution_location="local"` (no worker), asserts success.
+- Auto-surfaced in the in-app template browser (`GET /templates/`, `POST /templates/{id}/create`).
+- Author flows by building them in-process and `FlowGraph.save_flow()` — never hand-write node bodies.
+
+**Python examples** — committed at `docs/examples/.py` (Docker-dependent: `docs/examples/integrations/`):
+- One-line docstring; doc-visible code between `# --8<-- [start:example]` / `# --8<-- [end:example]`; real assertions BELOW the end marker (tests run them, pages never show them).
+- Use `import flowfile as ff` and only `ff`-namespace exports; repo-root-relative data paths (`data/templates/...`); runner pins CWD to repo root.
+- Runner: `flowfile_core/tests/docs_examples/test_docs_examples.py` (glob-parametrized; integrations gated on `test_utils` fixture availability — real Postgres/MinIO/etc., no mocks).
+- Pages include with `--8<-- "docs/examples/.py:example"`.
+
+**Data**: only committed, seeded datasets (`data/templates/*.csv` via `generate_template_data.py`; new generators use their own `random.Random(n)` and are called last so existing CSVs stay byte-identical — verify with `git diff`). Assert exact values for deterministic transforms; schema/shape only for ML and fuzzy outputs.
+
+**Reads are URL-first in user-facing examples** (maintainer-directed, 2026-07): sample reads use the public raw-GitHub URL (`TEMPLATE_DATA_BASE_URL` in `templates/data_downloader.py` + filename), not repo-relative paths — so a pasted snippet runs for pip-install users with no checkout, and flows opened in the WASM demo fetch data instead of embedding it (the share-link stays ~1 KB). The test runner skips (never mocks) URL-bearing examples when the URL is unreachable — which includes "not merged to main yet"; they execute for real once the data is public. Repo-relative paths remain fine for contributor-facing examples that already assume a checkout.
+
+**Add-a-worked-example recipe** (the "blog post" flow — no new test code, ever):
+1. Pick/extend a committed dataset.
+2. Build the flow in-process, save, placeholder-ize paths, add `_template_meta` → drop into `data/templates/flows/`.
+3. Optional Python twin in `docs/examples/`.
+4. Run `pytest flowfile_core/tests/templates/ flowfile_core/tests/docs_examples/ -q`.
+5. Write the page from the fixed skeleton (§5) under the owning persona tab; add the `nav:` entry and a gallery-index row.
+
+## 5. Worked-example page skeleton (fixed)
+
+```markdown
+#
+
+
+
+**Flow:** [`.yaml`](https://github.com/edwardvaneechoud/Flowfile/blob/main/data/templates/flows/.yaml) ·
+In-app: Create → From template → "" · Data: `data/templates/.csv`
+
+
+
+## The data
+## The flow
+## Run it
+## The result
+## In Python
+## Variations
+```
+
+## 6. Review checklist (run per touched page)
+
+0. Filler scan: zero generic tip lists, zero stating-the-obvious lines, no section that merely restates a sibling (§1.13–14). How-to pages read as short concrete steps.
+1. Every factual claim is timeless or verified against §3 (spot-check at least the counts, labels, signatures, defaults).
+2. Every runnable code block is a snippet include; inline fragments use verified syntax/labels.
+3. Zero register-1 markers: emoji headings/bullets, hype adjectives, inline-styled divs, "coming soon", unverifiable stats.
+4. Links resolve; anchors exist; page is in `nav:`; moves have redirect mappings.
+5. Numbers are pointers or date-stamped.
+6. Image spots are placeholders or existing assets — no image files created/edited.
+7. `FLOWFILE_SKIP_STARTUP_MIGRATION=1 poetry run mkdocs build` exits 0 with no new WARNINGs (snippets `check_paths` makes missing includes fatal).
+8. If examples were touched: `poetry run pytest flowfile_core/tests/templates/ flowfile_core/tests/docs_examples/ -q` green.
+
+## Provenance
+
+Distilled from a full-site audit + adversarial fact-check (361 claims verified against source) and a product-surface sweep, 2026-07-03, app version 0.12.7. The §3 index entries are the exact locations that resolved those claims. Re-verify fast-rotting values (counts, versions, model names) against their §3 source before quoting them in new prose.
diff --git a/.gitignore b/.gitignore
index a66c3e446..3e50c1950 100644
--- a/.gitignore
+++ b/.gitignore
@@ -104,3 +104,4 @@ error-linux
# Signing checklist holds real cert/notarization secrets — never commit
flowfile_frontend/src-tauri/SIGNING_CHECKLIST.md
+/.claude/agents/
diff --git a/data/templates/flows/sales_pipeline.yaml b/data/templates/flows/sales_pipeline.yaml
new file mode 100644
index 000000000..c9757eab6
--- /dev/null
+++ b/data/templates/flows/sales_pipeline.yaml
@@ -0,0 +1,111 @@
+flowfile_version: 0.8.2
+flowfile_id: 12
+flowfile_name: Sales Pipeline
+flowfile_settings:
+ description: null
+ execution_mode: Development
+ execution_location: local
+ auto_save: false
+ show_detailed_progress: true
+ max_parallel_workers: 4
+ source_registration_id: null
+ parameters: []
+nodes:
+- id: 1
+ type: read
+ is_start_node: true
+ description: ''
+ node_reference: null
+ x_position: 0
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: []
+ outputs: [2]
+ setting_input:
+ cache_results: false
+ received_file:
+ path: __TEMPLATE_DATA_DIR__/supermarket_sales.csv
+ file_type: csv
+ table_settings:
+ file_type: csv
+- id: 2
+ type: unique
+ is_start_node: false
+ description: ''
+ node_reference: null
+ x_position: 250
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [1]
+ outputs: [3]
+ setting_input:
+ cache_results: false
+ unique_input:
+ columns: null
+ strategy: any
+- id: 3
+ type: filter
+ is_start_node: false
+ description: ''
+ node_reference: null
+ x_position: 500
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [2]
+ outputs: [4]
+ setting_input:
+ cache_results: false
+ filter_input:
+ mode: advanced
+ advanced_filter: '[quantity] > 7'
+- id: 4
+ type: group_by
+ is_start_node: false
+ description: ''
+ node_reference: null
+ x_position: 750
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [3]
+ outputs: [5]
+ setting_input:
+ cache_results: false
+ groupby_input:
+ agg_cols:
+ - old_name: city
+ agg: groupby
+ new_name: city
+ - old_name: gross_income
+ agg: sum
+ new_name: total_income
+ - old_name: gross_income
+ agg: median
+ new_name: median_income
+- id: 5
+ type: explore_data
+ is_start_node: false
+ description: 'Explore the output!'
+ node_reference: null
+ x_position: 1000
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [4]
+ outputs: []
+ setting_input:
+ cache_results: false
+ graphic_walker_input: null
+_template_meta:
+ template_id: sales_pipeline
+ name: 'Sales pipeline: clean, filter, aggregate'
+ description: Drop duplicate sales rows, keep high-quantity orders, then total and
+ median gross income per city.
+ category: Beginner
+ tags: [unique, filter, group_by]
+ node_count: 5
+ icon: insights
+_required_csv_files: [supermarket_sales.csv]
diff --git a/data/templates/flows/sales_to_catalog.yaml b/data/templates/flows/sales_to_catalog.yaml
new file mode 100644
index 000000000..7302c4588
--- /dev/null
+++ b/data/templates/flows/sales_to_catalog.yaml
@@ -0,0 +1,85 @@
+flowfile_version: 0.8.2
+flowfile_id: 13
+flowfile_name: Sales To Catalog
+flowfile_settings:
+ description: null
+ execution_mode: Development
+ execution_location: local
+ auto_save: false
+ show_detailed_progress: true
+ max_parallel_workers: 4
+ source_registration_id: null
+ parameters: []
+nodes:
+- id: 1
+ type: read
+ is_start_node: true
+ description: ''
+ node_reference: null
+ x_position: 0
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: []
+ outputs: [2]
+ setting_input:
+ cache_results: false
+ received_file:
+ path: __TEMPLATE_DATA_DIR__/supermarket_sales.csv
+ file_type: csv
+ table_settings:
+ file_type: csv
+- id: 2
+ type: group_by
+ is_start_node: false
+ description: ''
+ node_reference: null
+ x_position: 250
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [1]
+ outputs: [3]
+ setting_input:
+ cache_results: false
+ groupby_input:
+ agg_cols:
+ - old_name: city
+ agg: groupby
+ new_name: city
+ - old_name: product_line
+ agg: groupby
+ new_name: product_line
+ - old_name: gross_income
+ agg: sum
+ new_name: total_income
+ - old_name: quantity
+ agg: sum
+ new_name: units_sold
+- id: 3
+ type: catalog_writer
+ is_start_node: false
+ description: ''
+ node_reference: null
+ x_position: 500
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [2]
+ outputs: []
+ setting_input:
+ cache_results: false
+ catalog_write_settings:
+ table_name: sales_by_city
+ namespace_full_name: General.default
+ write_mode: overwrite
+_template_meta:
+ template_id: sales_to_catalog
+ name: Sales to Catalog
+ description: Aggregate sales by city and product line, then publish the result as
+ a catalog table.
+ category: Intermediate
+ tags: [group_by, catalog_writer]
+ node_count: 3
+ icon: inventory
+_required_csv_files: [supermarket_sales.csv]
diff --git a/data/templates/generate_template_data.py b/data/templates/generate_template_data.py
index ac745d6b3..cf04e9f4f 100644
--- a/data/templates/generate_template_data.py
+++ b/data/templates/generate_template_data.py
@@ -433,6 +433,54 @@ def generate_customer_churn():
)
+def generate_supermarket_sales():
+ """~1000 supermarket sales rows for the flagship docs example.
+
+ Uses a dedicated RNG (seed 43) so it doesn't perturb the global-seed
+ outputs above; must run LAST in __main__.
+ """
+ rng = random.Random(43)
+ cities = ["Yangon", "Naypyitaw", "Mandalay", "Bago", "Taunggyi"]
+ customer_types = ["Member", "Normal"]
+ product_lines = [
+ "Health and beauty",
+ "Electronic accessories",
+ "Home and lifestyle",
+ "Sports and travel",
+ "Food and beverages",
+ "Fashion accessories",
+ ]
+ start = datetime(2024, 1, 1)
+ end = datetime(2024, 12, 31)
+ delta_days = (end - start).days
+
+ rows = []
+ for i in range(1, 1001):
+ unit_price = round(rng.uniform(10.0, 100.0), 2)
+ quantity = rng.randint(1, 10)
+ gross_income = round(unit_price * quantity * 0.05, 2)
+ date = (start + timedelta(days=rng.randint(0, delta_days))).strftime("%Y-%m-%d")
+ rows.append([
+ f"INV-{i:05d}",
+ rng.choice(cities),
+ rng.choice(customer_types),
+ rng.choice(product_lines),
+ unit_price,
+ quantity,
+ gross_income,
+ date,
+ ])
+
+ for _ in range(30):
+ rows.append(list(rng.choice(rows)))
+ rng.shuffle(rows)
+ write_csv(
+ "supermarket_sales.csv",
+ ["invoice_id", "city", "customer_type", "product_line", "unit_price", "quantity", "gross_income", "date"],
+ rows,
+ )
+
+
if __name__ == "__main__":
print("Generating template data files...")
generate_sales_data()
@@ -446,4 +494,5 @@ def generate_customer_churn():
generate_fuzzy_match_data()
generate_house_prices()
generate_customer_churn()
+ generate_supermarket_sales()
print("Done!")
diff --git a/data/templates/supermarket_sales.csv b/data/templates/supermarket_sales.csv
new file mode 100644
index 000000000..b8ae7f48f
--- /dev/null
+++ b/data/templates/supermarket_sales.csv
@@ -0,0 +1,1031 @@
+invoice_id,city,customer_type,product_line,unit_price,quantity,gross_income,date
+INV-00516,Bago,Normal,Health and beauty,57.16,4,11.43,2024-01-11
+INV-00163,Bago,Normal,Home and lifestyle,34.84,7,12.19,2024-12-24
+INV-00047,Bago,Normal,Food and beverages,43.84,8,17.54,2024-11-19
+INV-00963,Mandalay,Normal,Food and beverages,37.55,10,18.78,2024-08-05
+INV-00882,Yangon,Normal,Fashion accessories,33.31,7,11.66,2024-05-25
+INV-00370,Mandalay,Member,Fashion accessories,51.9,6,15.57,2024-06-21
+INV-00337,Mandalay,Member,Home and lifestyle,73.47,3,11.02,2024-03-30
+INV-00257,Bago,Member,Food and beverages,69.91,7,24.47,2024-03-23
+INV-00684,Naypyitaw,Member,Sports and travel,45.13,2,4.51,2024-09-10
+INV-00663,Naypyitaw,Normal,Health and beauty,24.56,7,8.6,2024-09-26
+INV-00519,Naypyitaw,Normal,Home and lifestyle,65.29,5,16.32,2024-11-07
+INV-00087,Taunggyi,Member,Food and beverages,44.33,9,19.95,2024-12-24
+INV-00725,Taunggyi,Normal,Electronic accessories,23.49,3,3.52,2024-02-28
+INV-00108,Yangon,Normal,Sports and travel,24.78,8,9.91,2024-05-03
+INV-00431,Bago,Normal,Food and beverages,54.58,4,10.92,2024-04-16
+INV-00862,Mandalay,Normal,Fashion accessories,37.35,9,16.81,2024-01-17
+INV-00331,Yangon,Member,Food and beverages,14.33,1,0.72,2024-12-31
+INV-00405,Mandalay,Normal,Home and lifestyle,49.62,7,17.37,2024-12-05
+INV-00789,Naypyitaw,Normal,Sports and travel,95.36,3,14.3,2024-07-30
+INV-00485,Mandalay,Member,Food and beverages,73.94,9,33.27,2024-08-28
+INV-00306,Mandalay,Member,Food and beverages,61.47,9,27.66,2024-04-16
+INV-00623,Taunggyi,Member,Fashion accessories,12.22,4,2.44,2024-02-15
+INV-00755,Taunggyi,Member,Food and beverages,35.17,2,3.52,2024-02-02
+INV-00849,Mandalay,Member,Home and lifestyle,67.49,9,30.37,2024-10-23
+INV-00942,Bago,Normal,Food and beverages,88.31,3,13.25,2024-06-09
+INV-00900,Taunggyi,Normal,Sports and travel,15.83,8,6.33,2024-12-27
+INV-00269,Naypyitaw,Normal,Food and beverages,63.25,8,25.3,2024-03-26
+INV-00220,Taunggyi,Normal,Fashion accessories,41.63,3,6.24,2024-06-02
+INV-00402,Naypyitaw,Normal,Electronic accessories,78.94,7,27.63,2024-07-30
+INV-00715,Bago,Normal,Fashion accessories,12.29,6,3.69,2024-12-13
+INV-00827,Taunggyi,Normal,Sports and travel,15.59,3,2.34,2024-04-29
+INV-00599,Naypyitaw,Member,Electronic accessories,98.11,2,9.81,2024-07-12
+INV-00734,Naypyitaw,Member,Home and lifestyle,85.49,6,25.65,2024-03-28
+INV-00933,Mandalay,Member,Food and beverages,42.06,5,10.52,2024-08-09
+INV-00346,Yangon,Normal,Fashion accessories,64.95,7,22.73,2024-07-12
+INV-00064,Taunggyi,Member,Fashion accessories,45.23,3,6.78,2024-01-06
+INV-00449,Naypyitaw,Normal,Electronic accessories,73.04,3,10.96,2024-03-10
+INV-00451,Mandalay,Member,Fashion accessories,65.08,4,13.02,2024-12-07
+INV-00194,Bago,Member,Food and beverages,10.52,8,4.21,2024-12-08
+INV-00854,Taunggyi,Normal,Health and beauty,67.4,5,16.85,2024-12-18
+INV-00793,Naypyitaw,Normal,Electronic accessories,39.95,6,11.99,2024-12-07
+INV-00842,Bago,Normal,Electronic accessories,52.19,9,23.49,2024-04-12
+INV-00794,Mandalay,Member,Health and beauty,82.87,5,20.72,2024-03-28
+INV-00414,Yangon,Normal,Fashion accessories,18.77,5,4.69,2024-04-29
+INV-00168,Yangon,Member,Food and beverages,41.11,5,10.28,2024-10-25
+INV-00284,Naypyitaw,Normal,Health and beauty,53.27,4,10.65,2024-05-07
+INV-00879,Naypyitaw,Normal,Fashion accessories,10.46,2,1.05,2024-01-28
+INV-00835,Taunggyi,Normal,Health and beauty,37.07,3,5.56,2024-02-18
+INV-00044,Mandalay,Member,Fashion accessories,44.96,9,20.23,2024-04-05
+INV-00810,Yangon,Member,Health and beauty,17.54,5,4.38,2024-03-24
+INV-00430,Taunggyi,Member,Sports and travel,40.38,3,6.06,2024-04-26
+INV-00128,Taunggyi,Normal,Health and beauty,45.69,7,15.99,2024-12-26
+INV-00409,Bago,Normal,Sports and travel,42.45,7,14.86,2024-05-22
+INV-00587,Bago,Normal,Home and lifestyle,27.18,9,12.23,2024-05-10
+INV-00192,Mandalay,Normal,Electronic accessories,62.05,10,31.03,2024-07-27
+INV-00029,Naypyitaw,Member,Electronic accessories,22.05,10,11.03,2024-03-18
+INV-00670,Taunggyi,Member,Food and beverages,89.56,8,35.82,2024-12-11
+INV-00434,Naypyitaw,Member,Fashion accessories,65.45,6,19.64,2024-07-26
+INV-00496,Taunggyi,Normal,Fashion accessories,58.79,7,20.58,2024-01-25
+INV-00021,Naypyitaw,Normal,Electronic accessories,73.56,1,3.68,2024-12-08
+INV-00310,Taunggyi,Normal,Food and beverages,23.38,6,7.01,2024-04-09
+INV-00271,Taunggyi,Normal,Health and beauty,71.78,6,21.53,2024-08-18
+INV-00869,Bago,Normal,Fashion accessories,63.59,1,3.18,2024-02-21
+INV-00378,Yangon,Member,Food and beverages,64.73,6,19.42,2024-08-03
+INV-00726,Bago,Member,Health and beauty,99.97,4,19.99,2024-06-05
+INV-00322,Yangon,Member,Sports and travel,36.11,10,18.06,2024-07-12
+INV-00600,Bago,Member,Fashion accessories,58.11,1,2.91,2024-03-17
+INV-00690,Yangon,Member,Home and lifestyle,25.6,10,12.8,2024-11-07
+INV-00730,Yangon,Member,Food and beverages,85.96,4,17.19,2024-12-26
+INV-00312,Taunggyi,Normal,Food and beverages,24.41,8,9.76,2024-09-20
+INV-00446,Mandalay,Member,Electronic accessories,53.16,1,2.66,2024-06-03
+INV-00285,Bago,Member,Electronic accessories,69.24,4,13.85,2024-06-28
+INV-00742,Yangon,Normal,Health and beauty,38.61,9,17.37,2024-07-01
+INV-00239,Bago,Member,Sports and travel,92.32,3,13.85,2024-11-19
+INV-00095,Taunggyi,Normal,Home and lifestyle,27.89,9,12.55,2024-10-27
+INV-00435,Yangon,Normal,Sports and travel,36.05,9,16.22,2024-06-19
+INV-00394,Yangon,Member,Fashion accessories,32.47,10,16.23,2024-12-30
+INV-00992,Yangon,Normal,Electronic accessories,38.51,7,13.48,2024-12-09
+INV-00480,Bago,Member,Home and lifestyle,98.25,7,34.39,2024-01-26
+INV-00765,Yangon,Normal,Electronic accessories,67.43,2,6.74,2024-12-01
+INV-00401,Bago,Normal,Food and beverages,72.67,1,3.63,2024-02-17
+INV-00455,Naypyitaw,Member,Health and beauty,11.2,7,3.92,2024-06-01
+INV-00011,Taunggyi,Member,Home and lifestyle,75.55,10,37.77,2024-02-06
+INV-00836,Bago,Normal,Home and lifestyle,52.16,4,10.43,2024-01-04
+INV-00259,Taunggyi,Member,Food and beverages,40.39,6,12.12,2024-04-10
+INV-00117,Naypyitaw,Member,Sports and travel,61.14,2,6.11,2024-12-16
+INV-00110,Yangon,Normal,Health and beauty,14.88,8,5.95,2024-11-26
+INV-00300,Mandalay,Member,Electronic accessories,65.82,6,19.75,2024-01-06
+INV-00551,Naypyitaw,Normal,Electronic accessories,36.36,3,5.45,2024-08-17
+INV-00139,Taunggyi,Member,Food and beverages,38.37,10,19.18,2024-03-22
+INV-00993,Bago,Member,Electronic accessories,38.57,1,1.93,2024-01-19
+INV-00815,Yangon,Member,Fashion accessories,50.83,5,12.71,2024-07-26
+INV-00024,Taunggyi,Member,Food and beverages,87.48,9,39.37,2024-09-14
+INV-00219,Mandalay,Member,Fashion accessories,49.32,4,9.86,2024-06-10
+INV-00178,Yangon,Normal,Electronic accessories,76.06,5,19.02,2024-06-07
+INV-00205,Mandalay,Normal,Health and beauty,47.31,6,14.19,2024-06-28
+INV-00203,Yangon,Member,Sports and travel,40.66,10,20.33,2024-09-15
+INV-00018,Bago,Member,Fashion accessories,13.01,4,2.6,2024-12-10
+INV-00031,Naypyitaw,Member,Fashion accessories,73.3,3,10.99,2024-06-12
+INV-00769,Naypyitaw,Normal,Sports and travel,76.63,3,11.49,2024-12-31
+INV-00974,Naypyitaw,Member,Fashion accessories,45.09,9,20.29,2024-09-08
+INV-00688,Mandalay,Normal,Fashion accessories,40.22,7,14.08,2024-12-22
+INV-00874,Naypyitaw,Member,Sports and travel,37.18,10,18.59,2024-11-24
+INV-00344,Mandalay,Normal,Home and lifestyle,82.13,9,36.96,2024-02-27
+INV-00861,Naypyitaw,Member,Health and beauty,62.87,4,12.57,2024-12-03
+INV-00918,Bago,Normal,Sports and travel,60.83,9,27.37,2024-08-05
+INV-00148,Yangon,Member,Fashion accessories,17.12,6,5.14,2024-06-11
+INV-00707,Bago,Member,Electronic accessories,14.56,8,5.82,2024-05-05
+INV-00652,Yangon,Normal,Sports and travel,88.8,2,8.88,2024-09-29
+INV-00334,Taunggyi,Member,Home and lifestyle,28.57,5,7.14,2024-08-31
+INV-00019,Taunggyi,Normal,Home and lifestyle,94.17,7,32.96,2024-05-13
+INV-00982,Taunggyi,Normal,Fashion accessories,22.27,2,2.23,2024-01-28
+INV-00978,Taunggyi,Member,Health and beauty,98.36,2,9.84,2024-08-17
+INV-00362,Yangon,Normal,Health and beauty,35.12,7,12.29,2024-05-31
+INV-00175,Mandalay,Normal,Electronic accessories,48.78,5,12.2,2024-06-13
+INV-00422,Yangon,Normal,Home and lifestyle,98.58,3,14.79,2024-07-01
+INV-00561,Mandalay,Normal,Health and beauty,96.06,6,28.82,2024-03-26
+INV-00500,Taunggyi,Member,Sports and travel,44.81,5,11.2,2024-04-02
+INV-00695,Yangon,Normal,Home and lifestyle,42.88,7,15.01,2024-10-29
+INV-00581,Yangon,Member,Electronic accessories,17.41,6,5.22,2024-12-08
+INV-00198,Mandalay,Member,Health and beauty,86.31,6,25.89,2024-07-28
+INV-00582,Yangon,Normal,Home and lifestyle,67.0,9,30.15,2024-10-26
+INV-00464,Bago,Member,Electronic accessories,33.27,4,6.65,2024-12-16
+INV-00181,Bago,Member,Home and lifestyle,14.5,2,1.45,2024-06-03
+INV-00901,Bago,Normal,Sports and travel,41.2,4,8.24,2024-05-27
+INV-00109,Naypyitaw,Normal,Food and beverages,39.23,8,15.69,2024-08-01
+INV-00964,Naypyitaw,Member,Sports and travel,40.72,9,18.32,2024-05-16
+INV-00989,Naypyitaw,Member,Electronic accessories,57.87,10,28.93,2024-12-29
+INV-00934,Taunggyi,Member,Sports and travel,83.2,9,37.44,2024-12-02
+INV-00593,Naypyitaw,Normal,Sports and travel,92.41,2,9.24,2024-03-14
+INV-00682,Yangon,Normal,Sports and travel,68.65,2,6.87,2024-04-08
+INV-00763,Naypyitaw,Member,Food and beverages,62.16,1,3.11,2024-01-23
+INV-00949,Yangon,Member,Fashion accessories,45.77,5,11.44,2024-11-27
+INV-00702,Mandalay,Normal,Health and beauty,48.8,4,9.76,2024-07-14
+INV-00639,Taunggyi,Member,Fashion accessories,43.5,6,13.05,2024-06-21
+INV-00263,Bago,Member,Home and lifestyle,20.03,2,2.0,2024-02-15
+INV-00186,Yangon,Normal,Food and beverages,65.3,8,26.12,2024-06-15
+INV-00681,Naypyitaw,Normal,Health and beauty,26.57,2,2.66,2024-03-12
+INV-00723,Naypyitaw,Normal,Sports and travel,62.57,5,15.64,2024-02-07
+INV-00556,Mandalay,Member,Health and beauty,17.7,10,8.85,2024-10-13
+INV-00891,Mandalay,Normal,Food and beverages,14.41,2,1.44,2024-08-25
+INV-00926,Mandalay,Normal,Food and beverages,83.06,7,29.07,2024-06-11
+INV-00143,Naypyitaw,Normal,Sports and travel,12.75,8,5.1,2024-01-06
+INV-00522,Bago,Member,Electronic accessories,57.8,10,28.9,2024-10-29
+INV-00997,Taunggyi,Member,Sports and travel,89.35,6,26.8,2024-11-14
+INV-00825,Yangon,Member,Fashion accessories,58.36,5,14.59,2024-06-28
+INV-00417,Yangon,Normal,Electronic accessories,42.34,6,12.7,2024-03-14
+INV-00520,Yangon,Normal,Health and beauty,90.33,6,27.1,2024-08-20
+INV-00689,Yangon,Member,Home and lifestyle,49.58,7,17.35,2024-11-14
+INV-00984,Mandalay,Normal,Sports and travel,38.94,6,11.68,2024-07-30
+INV-00061,Yangon,Member,Health and beauty,16.8,2,1.68,2024-08-24
+INV-00408,Naypyitaw,Normal,Home and lifestyle,11.36,1,0.57,2024-07-10
+INV-00512,Mandalay,Member,Home and lifestyle,99.98,7,34.99,2024-06-29
+INV-00248,Mandalay,Member,Fashion accessories,85.99,6,25.8,2024-02-19
+INV-00548,Yangon,Normal,Electronic accessories,83.16,1,4.16,2024-11-05
+INV-00701,Yangon,Normal,Sports and travel,30.51,3,4.58,2024-03-26
+INV-00325,Naypyitaw,Member,Sports and travel,15.68,4,3.14,2024-02-12
+INV-00426,Yangon,Member,Fashion accessories,33.05,9,14.87,2024-06-08
+INV-00439,Yangon,Member,Sports and travel,68.22,3,10.23,2024-05-23
+INV-00945,Naypyitaw,Member,Electronic accessories,35.3,6,10.59,2024-03-11
+INV-00796,Taunggyi,Normal,Sports and travel,46.12,1,2.31,2024-08-22
+INV-00843,Naypyitaw,Member,Home and lifestyle,54.48,4,10.9,2024-05-16
+INV-00035,Yangon,Member,Fashion accessories,65.25,3,9.79,2024-11-06
+INV-00875,Yangon,Member,Fashion accessories,80.96,7,28.34,2024-01-07
+INV-00590,Yangon,Member,Sports and travel,38.56,7,13.5,2024-06-04
+INV-00124,Yangon,Member,Electronic accessories,88.53,7,30.99,2024-04-15
+INV-00147,Taunggyi,Member,Electronic accessories,37.12,6,11.14,2024-11-17
+INV-00494,Yangon,Member,Sports and travel,57.36,9,25.81,2024-06-05
+INV-00399,Mandalay,Member,Fashion accessories,49.83,6,14.95,2024-12-10
+INV-00022,Bago,Member,Health and beauty,41.09,4,8.22,2024-07-15
+INV-00046,Naypyitaw,Normal,Health and beauty,78.16,5,19.54,2024-02-18
+INV-00893,Naypyitaw,Normal,Home and lifestyle,22.38,4,4.48,2024-02-08
+INV-00749,Mandalay,Normal,Health and beauty,89.54,8,35.82,2024-11-11
+INV-00700,Yangon,Member,Sports and travel,22.26,9,10.02,2024-08-19
+INV-00122,Yangon,Member,Home and lifestyle,43.32,6,13.0,2024-02-06
+INV-00275,Bago,Member,Home and lifestyle,84.89,8,33.96,2024-08-17
+INV-00166,Mandalay,Normal,Electronic accessories,58.78,6,17.63,2024-01-18
+INV-00974,Naypyitaw,Member,Fashion accessories,45.09,9,20.29,2024-09-08
+INV-00884,Yangon,Member,Food and beverages,41.66,5,10.41,2024-09-05
+INV-00947,Taunggyi,Normal,Electronic accessories,24.26,9,10.92,2024-10-16
+INV-00228,Taunggyi,Member,Sports and travel,66.11,8,26.44,2024-04-12
+INV-00969,Bago,Normal,Health and beauty,85.32,1,4.27,2024-08-06
+INV-00484,Mandalay,Normal,Fashion accessories,43.43,7,15.2,2024-03-13
+INV-00302,Naypyitaw,Normal,Food and beverages,17.87,10,8.94,2024-03-28
+INV-00437,Mandalay,Member,Fashion accessories,46.06,4,9.21,2024-10-05
+INV-00559,Mandalay,Member,Home and lifestyle,16.69,2,1.67,2024-09-11
+INV-00091,Bago,Normal,Home and lifestyle,15.51,3,2.33,2024-11-16
+INV-00518,Yangon,Member,Sports and travel,97.34,5,24.34,2024-08-06
+INV-00158,Naypyitaw,Member,Home and lifestyle,12.43,9,5.59,2024-08-30
+INV-00679,Bago,Member,Food and beverages,64.34,5,16.09,2024-02-27
+INV-00564,Bago,Member,Food and beverages,58.66,5,14.66,2024-06-24
+INV-00391,Taunggyi,Normal,Health and beauty,95.75,7,33.51,2024-12-13
+INV-00970,Naypyitaw,Member,Sports and travel,82.69,4,16.54,2024-10-06
+INV-00111,Bago,Normal,Electronic accessories,89.72,5,22.43,2024-10-13
+INV-00131,Bago,Member,Food and beverages,93.84,4,18.77,2024-07-26
+INV-00367,Mandalay,Member,Sports and travel,95.78,5,23.95,2024-04-16
+INV-00005,Bago,Normal,Food and beverages,56.12,8,22.45,2024-03-06
+INV-00272,Taunggyi,Normal,Sports and travel,23.84,9,10.73,2024-01-28
+INV-00479,Naypyitaw,Member,Fashion accessories,90.22,5,22.56,2024-04-30
+INV-00629,Bago,Member,Home and lifestyle,92.65,3,13.9,2024-07-16
+INV-00027,Bago,Member,Health and beauty,66.02,4,13.2,2024-07-11
+INV-00486,Naypyitaw,Member,Fashion accessories,53.29,10,26.64,2024-02-17
+INV-00050,Yangon,Member,Home and lifestyle,23.26,3,3.49,2024-07-02
+INV-00721,Bago,Member,Electronic accessories,22.56,5,5.64,2024-04-21
+INV-00125,Bago,Member,Food and beverages,40.56,10,20.28,2024-11-08
+INV-00637,Taunggyi,Member,Food and beverages,16.44,2,1.64,2024-06-25
+INV-00258,Bago,Normal,Health and beauty,38.86,8,15.54,2024-07-02
+INV-00944,Taunggyi,Member,Sports and travel,90.96,7,31.84,2024-08-15
+INV-00786,Yangon,Member,Food and beverages,23.95,8,9.58,2024-02-21
+INV-00547,Taunggyi,Normal,Electronic accessories,87.46,9,39.36,2024-11-18
+INV-00589,Naypyitaw,Member,Sports and travel,31.49,5,7.87,2024-02-25
+INV-00133,Yangon,Normal,Fashion accessories,25.75,4,5.15,2024-02-09
+INV-00760,Taunggyi,Normal,Home and lifestyle,69.8,8,27.92,2024-04-22
+INV-00532,Naypyitaw,Member,Home and lifestyle,13.67,7,4.78,2024-12-16
+INV-00116,Bago,Member,Health and beauty,24.37,1,1.22,2024-10-09
+INV-00217,Taunggyi,Normal,Health and beauty,82.5,9,37.12,2024-07-07
+INV-00333,Mandalay,Member,Electronic accessories,78.24,10,39.12,2024-12-07
+INV-00811,Yangon,Normal,Electronic accessories,89.94,7,31.48,2024-12-06
+INV-00803,Bago,Member,Fashion accessories,77.07,2,7.71,2024-03-01
+INV-00666,Taunggyi,Normal,Health and beauty,33.86,4,6.77,2024-12-24
+INV-00665,Taunggyi,Normal,Home and lifestyle,28.52,7,9.98,2024-08-28
+INV-00185,Taunggyi,Normal,Sports and travel,44.24,10,22.12,2024-09-26
+INV-00004,Naypyitaw,Member,Electronic accessories,86.58,2,8.66,2024-07-11
+INV-00470,Mandalay,Normal,Food and beverages,24.82,8,9.93,2024-07-07
+INV-00852,Bago,Member,Sports and travel,37.58,7,13.15,2024-08-05
+INV-00325,Naypyitaw,Member,Sports and travel,15.68,4,3.14,2024-02-12
+INV-00014,Yangon,Normal,Sports and travel,75.8,10,37.9,2024-06-06
+INV-00541,Taunggyi,Member,Health and beauty,14.19,4,2.84,2024-05-31
+INV-00873,Naypyitaw,Normal,Home and lifestyle,26.39,5,6.6,2024-12-25
+INV-00871,Taunggyi,Normal,Electronic accessories,57.49,3,8.62,2024-01-03
+INV-00937,Taunggyi,Member,Sports and travel,14.8,7,5.18,2024-06-18
+INV-00127,Taunggyi,Member,Home and lifestyle,73.22,4,14.64,2024-06-19
+INV-00659,Mandalay,Member,Food and beverages,41.04,3,6.16,2024-12-17
+INV-00727,Yangon,Normal,Food and beverages,29.06,4,5.81,2024-05-13
+INV-00542,Naypyitaw,Normal,Health and beauty,70.05,8,28.02,2024-02-12
+INV-00039,Bago,Normal,Home and lifestyle,42.24,6,12.67,2024-02-21
+INV-00042,Naypyitaw,Member,Electronic accessories,67.37,3,10.11,2024-06-04
+INV-00319,Taunggyi,Normal,Health and beauty,28.15,3,4.22,2024-08-14
+INV-00820,Naypyitaw,Member,Electronic accessories,83.21,1,4.16,2024-05-07
+INV-00078,Bago,Normal,Health and beauty,91.21,5,22.8,2024-12-04
+INV-00240,Mandalay,Member,Fashion accessories,71.25,10,35.62,2024-07-28
+INV-00660,Mandalay,Member,Sports and travel,15.33,7,5.37,2024-01-28
+INV-00799,Mandalay,Member,Fashion accessories,25.75,2,2.58,2024-03-06
+INV-00155,Naypyitaw,Member,Health and beauty,16.25,7,5.69,2024-07-13
+INV-00067,Mandalay,Normal,Fashion accessories,73.04,10,36.52,2024-06-13
+INV-01000,Yangon,Normal,Home and lifestyle,45.12,10,22.56,2024-01-13
+INV-00193,Yangon,Member,Food and beverages,35.8,2,3.58,2024-02-06
+INV-00858,Yangon,Member,Food and beverages,19.22,7,6.73,2024-02-28
+INV-00114,Naypyitaw,Normal,Food and beverages,40.4,5,10.1,2024-07-02
+INV-00357,Naypyitaw,Member,Fashion accessories,62.83,3,9.42,2024-02-11
+INV-00657,Bago,Member,Sports and travel,92.3,1,4.62,2024-01-23
+INV-00936,Mandalay,Normal,Home and lifestyle,95.69,7,33.49,2024-05-27
+INV-00010,Yangon,Normal,Home and lifestyle,21.37,2,2.14,2024-06-12
+INV-00924,Naypyitaw,Normal,Health and beauty,23.8,4,4.76,2024-06-12
+INV-00932,Naypyitaw,Normal,Food and beverages,49.56,10,24.78,2024-05-14
+INV-00595,Naypyitaw,Normal,Electronic accessories,47.1,4,9.42,2024-06-26
+INV-00764,Bago,Member,Health and beauty,34.12,5,8.53,2024-05-08
+INV-00788,Naypyitaw,Normal,Sports and travel,76.56,5,19.14,2024-03-01
+INV-00578,Naypyitaw,Member,Home and lifestyle,39.3,10,19.65,2024-02-13
+INV-00778,Mandalay,Member,Health and beauty,37.29,9,16.78,2024-08-30
+INV-00206,Bago,Member,Fashion accessories,75.19,2,7.52,2024-08-15
+INV-00261,Naypyitaw,Normal,Health and beauty,35.01,7,12.25,2024-08-29
+INV-00647,Naypyitaw,Normal,Sports and travel,70.1,1,3.5,2024-09-12
+INV-00265,Mandalay,Normal,Food and beverages,40.61,7,14.21,2024-05-09
+INV-00961,Mandalay,Normal,Electronic accessories,15.13,6,4.54,2024-07-12
+INV-00550,Yangon,Normal,Health and beauty,51.38,5,12.85,2024-04-07
+INV-00737,Taunggyi,Normal,Food and beverages,97.14,7,34.0,2024-01-20
+INV-00280,Mandalay,Normal,Food and beverages,84.81,6,25.44,2024-07-24
+INV-00347,Mandalay,Normal,Home and lifestyle,71.68,3,10.75,2024-05-06
+INV-00655,Naypyitaw,Normal,Fashion accessories,44.4,8,17.76,2024-12-29
+INV-00631,Taunggyi,Member,Health and beauty,12.25,7,4.29,2024-12-02
+INV-00160,Mandalay,Member,Fashion accessories,17.72,4,3.54,2024-10-07
+INV-00513,Mandalay,Normal,Home and lifestyle,91.45,2,9.15,2024-07-06
+INV-00576,Yangon,Normal,Food and beverages,75.31,2,7.53,2024-06-08
+INV-00661,Taunggyi,Normal,Fashion accessories,75.23,6,22.57,2024-09-12
+INV-00759,Bago,Member,Sports and travel,96.07,8,38.43,2024-01-09
+INV-00237,Bago,Member,Fashion accessories,40.03,5,10.01,2024-06-20
+INV-00814,Taunggyi,Member,Electronic accessories,57.45,7,20.11,2024-10-31
+INV-00540,Bago,Member,Home and lifestyle,87.79,2,8.78,2024-07-01
+INV-00073,Yangon,Normal,Home and lifestyle,42.41,8,16.96,2024-05-24
+INV-00890,Mandalay,Member,Health and beauty,16.99,10,8.49,2024-09-06
+INV-00719,Mandalay,Member,Electronic accessories,15.16,10,7.58,2024-11-25
+INV-00088,Mandalay,Normal,Fashion accessories,84.01,6,25.2,2024-02-05
+INV-00173,Naypyitaw,Member,Sports and travel,38.46,2,3.85,2024-12-11
+INV-00033,Taunggyi,Normal,Food and beverages,75.04,8,30.02,2024-06-06
+INV-00201,Taunggyi,Normal,Electronic accessories,27.91,10,13.96,2024-06-07
+INV-00878,Bago,Member,Electronic accessories,50.21,7,17.57,2024-01-29
+INV-00713,Mandalay,Member,Food and beverages,33.38,9,15.02,2024-01-16
+INV-00161,Bago,Normal,Health and beauty,82.7,9,37.22,2024-05-01
+INV-00023,Yangon,Member,Food and beverages,51.25,1,2.56,2024-02-19
+INV-00467,Yangon,Normal,Health and beauty,71.4,5,17.85,2024-06-18
+INV-00733,Bago,Member,Sports and travel,38.36,10,19.18,2024-08-20
+INV-00382,Taunggyi,Normal,Electronic accessories,44.61,4,8.92,2024-12-14
+INV-00298,Mandalay,Member,Fashion accessories,41.38,3,6.21,2024-04-14
+INV-00466,Yangon,Normal,Electronic accessories,97.5,2,9.75,2024-03-09
+INV-00774,Mandalay,Member,Home and lifestyle,29.85,1,1.49,2024-06-11
+INV-00824,Yangon,Normal,Fashion accessories,84.57,10,42.28,2024-06-18
+INV-00899,Bago,Member,Health and beauty,55.23,8,22.09,2024-07-10
+INV-00184,Naypyitaw,Normal,Food and beverages,22.12,4,4.42,2024-12-05
+INV-00374,Naypyitaw,Member,Home and lifestyle,64.63,3,9.69,2024-05-10
+INV-00314,Yangon,Normal,Food and beverages,55.53,1,2.78,2024-10-17
+INV-00962,Bago,Member,Sports and travel,74.16,1,3.71,2024-08-23
+INV-00274,Mandalay,Normal,Electronic accessories,75.24,2,7.52,2024-08-22
+INV-00341,Bago,Normal,Food and beverages,94.3,7,33.01,2024-05-10
+INV-00207,Mandalay,Normal,Health and beauty,76.27,1,3.81,2024-07-01
+INV-00710,Taunggyi,Member,Sports and travel,12.56,6,3.77,2024-12-15
+INV-00864,Bago,Member,Electronic accessories,23.35,3,3.5,2024-04-29
+INV-00931,Bago,Normal,Health and beauty,33.37,2,3.34,2024-10-19
+INV-00856,Yangon,Member,Home and lifestyle,98.79,3,14.82,2024-09-07
+INV-00411,Naypyitaw,Normal,Electronic accessories,21.11,1,1.06,2024-03-14
+INV-00079,Mandalay,Member,Fashion accessories,11.85,1,0.59,2024-03-27
+INV-00499,Naypyitaw,Normal,Sports and travel,56.9,10,28.45,2024-08-13
+INV-00833,Naypyitaw,Normal,Electronic accessories,23.51,8,9.4,2024-08-30
+INV-00096,Naypyitaw,Normal,Health and beauty,30.6,4,6.12,2024-06-12
+INV-00350,Mandalay,Normal,Sports and travel,91.82,1,4.59,2024-11-04
+INV-00005,Bago,Normal,Food and beverages,56.12,8,22.45,2024-03-06
+INV-00698,Bago,Member,Home and lifestyle,24.75,3,3.71,2024-08-17
+INV-00145,Yangon,Normal,Food and beverages,13.04,8,5.22,2024-12-01
+INV-00048,Bago,Normal,Health and beauty,68.35,2,6.83,2024-01-05
+INV-00202,Taunggyi,Member,Home and lifestyle,80.66,9,36.3,2024-08-24
+INV-00544,Yangon,Member,Electronic accessories,67.93,2,6.79,2024-05-05
+INV-00099,Mandalay,Member,Health and beauty,45.12,1,2.26,2024-10-19
+INV-00183,Bago,Normal,Fashion accessories,43.63,1,2.18,2024-07-08
+INV-00798,Mandalay,Member,Electronic accessories,21.31,6,6.39,2024-10-07
+INV-00065,Yangon,Member,Fashion accessories,20.27,7,7.09,2024-01-20
+INV-00101,Mandalay,Member,Food and beverages,75.81,10,37.91,2024-08-06
+INV-00461,Mandalay,Normal,Food and beverages,54.69,8,21.88,2024-11-30
+INV-00303,Yangon,Normal,Home and lifestyle,44.15,3,6.62,2024-10-28
+INV-00771,Bago,Normal,Sports and travel,61.87,4,12.37,2024-08-29
+INV-00179,Taunggyi,Member,Electronic accessories,31.27,9,14.07,2024-12-17
+INV-00883,Naypyitaw,Member,Home and lifestyle,15.92,5,3.98,2024-04-11
+INV-00845,Bago,Member,Home and lifestyle,24.28,10,12.14,2024-04-30
+INV-00243,Taunggyi,Normal,Home and lifestyle,65.08,10,32.54,2024-10-10
+INV-00458,Taunggyi,Member,Electronic accessories,81.05,7,28.37,2024-02-03
+INV-00006,Taunggyi,Member,Electronic accessories,26.37,2,2.64,2024-01-26
+INV-00722,Bago,Member,Sports and travel,44.03,3,6.6,2024-06-19
+INV-00150,Naypyitaw,Normal,Health and beauty,80.42,9,36.19,2024-12-16
+INV-00075,Taunggyi,Normal,Home and lifestyle,53.1,4,10.62,2024-12-09
+INV-00986,Naypyitaw,Normal,Home and lifestyle,11.79,8,4.72,2024-11-25
+INV-00906,Mandalay,Member,Fashion accessories,22.29,6,6.69,2024-01-14
+INV-00001,Mandalay,Member,Sports and travel,13.47,3,2.02,2024-08-24
+INV-00311,Yangon,Normal,Health and beauty,87.25,8,34.9,2024-05-03
+INV-00762,Bago,Normal,Food and beverages,35.6,4,7.12,2024-07-20
+INV-00527,Mandalay,Normal,Fashion accessories,90.04,3,13.51,2024-06-12
+INV-00286,Bago,Member,Sports and travel,87.87,9,39.54,2024-04-07
+INV-00748,Mandalay,Member,Fashion accessories,44.94,2,4.49,2024-05-05
+INV-00474,Bago,Member,Fashion accessories,80.84,7,28.29,2024-12-22
+INV-00336,Mandalay,Member,Home and lifestyle,38.92,10,19.46,2024-07-02
+INV-00767,Yangon,Normal,Fashion accessories,65.71,8,26.28,2024-05-07
+INV-00643,Taunggyi,Normal,Electronic accessories,40.23,9,18.1,2024-10-16
+INV-00054,Naypyitaw,Member,Sports and travel,93.55,10,46.78,2024-04-18
+INV-00397,Mandalay,Normal,Food and beverages,39.15,5,9.79,2024-07-30
+INV-00094,Mandalay,Member,Electronic accessories,67.39,7,23.59,2024-06-20
+INV-00812,Yangon,Member,Home and lifestyle,83.12,2,8.31,2024-04-24
+INV-00808,Yangon,Member,Food and beverages,73.84,6,22.15,2024-09-19
+INV-00097,Mandalay,Member,Sports and travel,35.86,2,3.59,2024-09-03
+INV-00805,Bago,Normal,Sports and travel,97.45,9,43.85,2024-06-10
+INV-00070,Mandalay,Member,Health and beauty,36.86,6,11.06,2024-08-18
+INV-00938,Naypyitaw,Member,Sports and travel,39.89,8,15.96,2024-08-06
+INV-00267,Mandalay,Member,Home and lifestyle,90.58,9,40.76,2024-11-15
+INV-00154,Mandalay,Normal,Food and beverages,27.65,5,6.91,2024-01-27
+INV-00741,Bago,Member,Food and beverages,25.72,4,5.14,2024-12-30
+INV-00821,Naypyitaw,Member,Home and lifestyle,99.83,8,39.93,2024-03-14
+INV-00427,Bago,Normal,Home and lifestyle,17.89,5,4.47,2024-07-02
+INV-00894,Mandalay,Member,Home and lifestyle,67.57,5,16.89,2024-08-17
+INV-00137,Yangon,Normal,Sports and travel,97.45,8,38.98,2024-01-24
+INV-00552,Taunggyi,Member,Food and beverages,81.95,3,12.29,2024-07-07
+INV-00071,Mandalay,Normal,Health and beauty,94.54,7,33.09,2024-06-03
+INV-00080,Mandalay,Member,Food and beverages,41.48,10,20.74,2024-11-29
+INV-00644,Naypyitaw,Normal,Sports and travel,24.65,9,11.09,2024-10-23
+INV-00504,Bago,Normal,Health and beauty,76.34,9,34.35,2024-10-06
+INV-00282,Bago,Member,Health and beauty,24.41,4,4.88,2024-06-18
+INV-00403,Yangon,Member,Sports and travel,57.59,5,14.4,2024-01-05
+INV-00141,Mandalay,Member,Electronic accessories,53.0,10,26.5,2024-02-06
+INV-00851,Naypyitaw,Normal,Home and lifestyle,15.37,5,3.84,2024-09-26
+INV-00433,Taunggyi,Normal,Food and beverages,27.67,2,2.77,2024-10-31
+INV-00979,Taunggyi,Normal,Health and beauty,71.46,5,17.86,2024-12-31
+INV-00052,Mandalay,Member,Sports and travel,12.02,6,3.61,2024-04-14
+INV-00371,Yangon,Normal,Home and lifestyle,22.23,1,1.11,2024-01-02
+INV-00308,Yangon,Member,Fashion accessories,31.07,6,9.32,2024-11-17
+INV-00030,Mandalay,Member,Sports and travel,13.95,5,3.49,2024-09-02
+INV-00694,Taunggyi,Normal,Food and beverages,70.76,4,14.15,2024-10-28
+INV-00783,Bago,Normal,Home and lifestyle,90.23,1,4.51,2024-05-23
+INV-00459,Yangon,Normal,Food and beverages,39.62,9,17.83,2024-11-01
+INV-00115,Taunggyi,Member,Food and beverages,78.74,8,31.5,2024-09-16
+INV-00950,Yangon,Member,Health and beauty,36.99,9,16.65,2024-11-09
+INV-00156,Naypyitaw,Member,Food and beverages,62.01,2,6.2,2024-08-12
+INV-00445,Bago,Normal,Food and beverages,14.19,10,7.1,2024-07-08
+INV-00404,Naypyitaw,Normal,Sports and travel,78.22,3,11.73,2024-05-01
+INV-00938,Naypyitaw,Member,Sports and travel,39.89,8,15.96,2024-08-06
+INV-00672,Naypyitaw,Member,Sports and travel,49.83,9,22.42,2024-03-26
+INV-00016,Taunggyi,Normal,Home and lifestyle,76.11,6,22.83,2024-03-01
+INV-00107,Yangon,Normal,Fashion accessories,76.63,10,38.31,2024-09-19
+INV-00456,Bago,Member,Home and lifestyle,73.04,6,21.91,2024-04-10
+INV-00775,Taunggyi,Member,Food and beverages,81.56,8,32.62,2024-12-31
+INV-00804,Bago,Normal,Sports and travel,90.75,5,22.69,2024-04-12
+INV-00696,Yangon,Normal,Food and beverages,52.98,10,26.49,2024-07-02
+INV-00935,Bago,Member,Sports and travel,26.04,3,3.91,2024-01-12
+INV-00929,Bago,Member,Food and beverages,54.38,10,27.19,2024-01-09
+INV-00777,Taunggyi,Member,Fashion accessories,68.62,5,17.16,2024-10-30
+INV-00212,Yangon,Normal,Home and lifestyle,82.93,4,16.59,2024-02-12
+INV-00326,Taunggyi,Normal,Home and lifestyle,89.04,6,26.71,2024-06-12
+INV-00584,Mandalay,Normal,Electronic accessories,74.34,9,33.45,2024-12-22
+INV-00939,Naypyitaw,Member,Fashion accessories,17.16,9,7.72,2024-01-23
+INV-00526,Taunggyi,Normal,Home and lifestyle,27.18,4,5.44,2024-11-30
+INV-00501,Yangon,Normal,Electronic accessories,85.88,10,42.94,2024-11-11
+INV-00100,Naypyitaw,Member,Fashion accessories,52.7,2,5.27,2024-05-01
+INV-00381,Yangon,Normal,Health and beauty,23.84,3,3.58,2024-10-19
+INV-00872,Bago,Normal,Fashion accessories,79.4,2,7.94,2024-11-14
+INV-00718,Taunggyi,Normal,Sports and travel,67.71,3,10.16,2024-10-24
+INV-00625,Taunggyi,Normal,Fashion accessories,90.13,8,36.05,2024-10-17
+INV-00388,Naypyitaw,Normal,Food and beverages,71.7,5,17.93,2024-01-08
+INV-00667,Bago,Member,Home and lifestyle,89.0,6,26.7,2024-10-11
+INV-00448,Taunggyi,Normal,Electronic accessories,18.44,4,3.69,2024-12-05
+INV-00720,Naypyitaw,Member,Electronic accessories,12.27,6,3.68,2024-12-16
+INV-00008,Yangon,Member,Food and beverages,66.12,10,33.06,2024-11-30
+INV-00355,Taunggyi,Member,Home and lifestyle,35.85,4,7.17,2024-02-16
+INV-00991,Bago,Normal,Health and beauty,10.17,4,2.03,2024-10-22
+INV-00740,Yangon,Normal,Food and beverages,24.34,5,6.09,2024-09-19
+INV-00585,Mandalay,Member,Home and lifestyle,35.08,5,8.77,2024-03-09
+INV-00463,Taunggyi,Normal,Sports and travel,98.55,5,24.64,2024-02-20
+INV-00747,Yangon,Normal,Home and lifestyle,34.06,3,5.11,2024-04-29
+INV-00944,Taunggyi,Member,Sports and travel,90.96,7,31.84,2024-08-15
+INV-00678,Yangon,Member,Home and lifestyle,59.46,10,29.73,2024-09-05
+INV-00995,Bago,Member,Health and beauty,64.31,8,25.72,2024-02-02
+INV-00244,Mandalay,Member,Home and lifestyle,79.59,5,19.9,2024-08-27
+INV-00364,Taunggyi,Normal,Food and beverages,98.45,6,29.54,2024-10-01
+INV-00356,Bago,Member,Electronic accessories,44.24,5,11.06,2024-02-18
+INV-00372,Mandalay,Member,Fashion accessories,18.94,6,5.68,2024-08-16
+INV-00607,Yangon,Member,Fashion accessories,76.08,2,7.61,2024-09-01
+INV-00677,Taunggyi,Normal,Fashion accessories,81.86,6,24.56,2024-06-11
+INV-00215,Mandalay,Normal,Health and beauty,17.63,9,7.93,2024-12-04
+INV-00855,Yangon,Member,Health and beauty,69.3,5,17.32,2024-10-06
+INV-00752,Naypyitaw,Member,Electronic accessories,60.2,10,30.1,2024-12-29
+INV-00829,Taunggyi,Member,Food and beverages,27.2,8,10.88,2024-09-29
+INV-00462,Taunggyi,Member,Health and beauty,64.5,10,32.25,2024-06-01
+INV-00142,Bago,Member,Food and beverages,89.98,6,26.99,2024-09-02
+INV-00171,Bago,Member,Sports and travel,11.88,9,5.35,2024-03-24
+INV-00221,Bago,Normal,Home and lifestyle,79.46,1,3.97,2024-04-14
+INV-00262,Bago,Member,Food and beverages,20.67,8,8.27,2024-09-09
+INV-00232,Yangon,Member,Electronic accessories,14.15,9,6.37,2024-01-10
+INV-00442,Yangon,Normal,Health and beauty,41.17,2,4.12,2024-11-15
+INV-00608,Bago,Normal,Health and beauty,21.35,7,7.47,2024-02-13
+INV-00948,Bago,Normal,Fashion accessories,74.36,3,11.15,2024-07-02
+INV-00102,Bago,Member,Electronic accessories,32.37,5,8.09,2024-01-15
+INV-00782,Bago,Normal,Food and beverages,35.73,3,5.36,2024-01-15
+INV-00905,Taunggyi,Normal,Electronic accessories,20.37,2,2.04,2024-05-31
+INV-00342,Bago,Normal,Fashion accessories,52.21,5,13.05,2024-06-22
+INV-00098,Naypyitaw,Normal,Health and beauty,13.66,5,3.42,2024-06-16
+INV-00616,Naypyitaw,Member,Home and lifestyle,94.89,9,42.7,2024-11-23
+INV-00425,Naypyitaw,Member,Health and beauty,80.99,2,8.1,2024-10-14
+INV-00315,Bago,Member,Sports and travel,65.33,2,6.53,2024-12-30
+INV-00252,Naypyitaw,Member,Sports and travel,28.16,5,7.04,2024-10-09
+INV-00538,Taunggyi,Normal,Electronic accessories,18.67,9,8.4,2024-06-03
+INV-00999,Taunggyi,Member,Sports and travel,84.69,10,42.34,2024-05-31
+INV-00304,Taunggyi,Normal,Home and lifestyle,56.91,6,17.07,2024-03-31
+INV-00130,Bago,Normal,Electronic accessories,64.66,1,3.23,2024-08-25
+INV-00971,Bago,Member,Food and beverages,88.76,5,22.19,2024-02-03
+INV-00960,Mandalay,Member,Home and lifestyle,22.46,9,10.11,2024-12-08
+INV-00316,Mandalay,Normal,Food and beverages,25.32,2,2.53,2024-06-07
+INV-00943,Naypyitaw,Normal,Sports and travel,67.96,6,20.39,2024-08-16
+INV-00077,Taunggyi,Normal,Food and beverages,19.75,10,9.88,2024-09-17
+INV-00321,Yangon,Member,Home and lifestyle,37.03,3,5.55,2024-06-23
+INV-00998,Bago,Normal,Health and beauty,41.19,7,14.42,2024-11-28
+INV-00090,Mandalay,Normal,Home and lifestyle,23.86,8,9.54,2024-07-31
+INV-00692,Taunggyi,Normal,Food and beverages,58.69,8,23.48,2024-12-02
+INV-00416,Taunggyi,Normal,Sports and travel,48.7,5,12.18,2024-09-16
+INV-00904,Bago,Normal,Health and beauty,92.53,10,46.27,2024-05-12
+INV-00754,Bago,Normal,Health and beauty,64.46,1,3.22,2024-05-05
+INV-00807,Bago,Normal,Sports and travel,46.82,8,18.73,2024-02-11
+INV-00753,Naypyitaw,Member,Home and lifestyle,38.42,4,7.68,2024-12-15
+INV-00301,Yangon,Normal,Electronic accessories,78.56,10,39.28,2024-07-31
+INV-00920,Mandalay,Normal,Sports and travel,16.18,7,5.66,2024-08-30
+INV-00170,Yangon,Member,Food and beverages,11.38,2,1.14,2024-04-19
+INV-00618,Mandalay,Member,Sports and travel,95.33,6,28.6,2024-05-12
+INV-00468,Mandalay,Normal,Sports and travel,78.24,9,35.21,2024-01-06
+INV-00985,Mandalay,Member,Fashion accessories,28.72,9,12.92,2024-10-01
+INV-00658,Bago,Normal,Sports and travel,79.35,8,31.74,2024-07-11
+INV-00987,Naypyitaw,Normal,Food and beverages,36.03,3,5.4,2024-08-09
+INV-00068,Yangon,Member,Food and beverages,48.3,1,2.42,2024-08-12
+INV-00802,Bago,Normal,Electronic accessories,83.55,6,25.06,2024-02-23
+INV-00117,Naypyitaw,Member,Sports and travel,61.14,2,6.11,2024-12-16
+INV-00543,Naypyitaw,Normal,Health and beauty,18.12,4,3.62,2024-03-05
+INV-00787,Naypyitaw,Normal,Electronic accessories,30.42,10,15.21,2024-03-21
+INV-00360,Mandalay,Normal,Food and beverages,51.85,6,15.56,2024-08-05
+INV-00034,Taunggyi,Member,Electronic accessories,64.91,7,22.72,2024-09-22
+INV-00888,Taunggyi,Normal,Sports and travel,60.11,8,24.04,2024-06-28
+INV-00041,Taunggyi,Normal,Food and beverages,37.12,7,12.99,2024-05-02
+INV-00153,Taunggyi,Normal,Food and beverages,85.28,2,8.53,2024-01-07
+INV-00053,Yangon,Member,Health and beauty,51.84,1,2.59,2024-11-20
+INV-00913,Bago,Member,Fashion accessories,53.28,10,26.64,2024-02-13
+INV-00592,Yangon,Normal,Sports and travel,15.92,1,0.8,2024-02-26
+INV-00348,Bago,Normal,Fashion accessories,86.07,5,21.52,2024-05-08
+INV-00863,Naypyitaw,Normal,Sports and travel,57.77,9,26.0,2024-03-12
+INV-00675,Mandalay,Member,Health and beauty,97.82,10,48.91,2024-01-21
+INV-00296,Mandalay,Member,Home and lifestyle,62.07,9,27.93,2024-03-24
+INV-00640,Mandalay,Member,Food and beverages,41.42,9,18.64,2024-10-17
+INV-00975,Naypyitaw,Member,Home and lifestyle,17.62,3,2.64,2024-06-02
+INV-00299,Yangon,Normal,Health and beauty,55.9,7,19.57,2024-01-03
+INV-00959,Naypyitaw,Member,Food and beverages,94.62,3,14.19,2024-12-22
+INV-00886,Mandalay,Normal,Food and beverages,69.92,3,10.49,2024-02-16
+INV-00574,Taunggyi,Normal,Health and beauty,57.56,4,11.51,2024-04-03
+INV-00015,Yangon,Member,Electronic accessories,62.84,3,9.43,2024-01-02
+INV-00126,Mandalay,Member,Sports and travel,33.75,6,10.12,2024-01-22
+INV-00568,Mandalay,Member,Sports and travel,54.49,3,8.17,2024-08-30
+INV-00806,Yangon,Normal,Food and beverages,66.14,10,33.07,2024-08-24
+INV-00582,Yangon,Normal,Home and lifestyle,67.0,9,30.15,2024-10-26
+INV-00656,Bago,Normal,Health and beauty,63.92,8,25.57,2024-07-28
+INV-00384,Mandalay,Normal,Health and beauty,37.53,3,5.63,2024-10-05
+INV-00045,Bago,Normal,Food and beverages,56.39,3,8.46,2024-02-22
+INV-00624,Naypyitaw,Normal,Sports and travel,36.83,5,9.21,2024-07-30
+INV-00830,Mandalay,Member,Sports and travel,13.68,2,1.37,2024-10-28
+INV-00911,Naypyitaw,Member,Sports and travel,14.74,4,2.95,2024-07-21
+INV-00443,Mandalay,Member,Sports and travel,80.91,7,28.32,2024-08-30
+INV-00704,Yangon,Normal,Electronic accessories,49.96,6,14.99,2024-11-01
+INV-00885,Taunggyi,Member,Home and lifestyle,76.67,4,15.33,2024-04-10
+INV-00247,Mandalay,Normal,Electronic accessories,24.61,10,12.3,2024-03-15
+INV-00199,Bago,Normal,Electronic accessories,68.52,1,3.43,2024-02-22
+INV-00051,Taunggyi,Normal,Electronic accessories,39.79,1,1.99,2024-02-15
+INV-00421,Mandalay,Normal,Health and beauty,89.77,4,17.95,2024-03-13
+INV-00472,Taunggyi,Normal,Electronic accessories,65.55,7,22.94,2024-08-06
+INV-00687,Mandalay,Normal,Electronic accessories,83.59,6,25.08,2024-11-27
+INV-00038,Naypyitaw,Member,Home and lifestyle,51.26,5,12.82,2024-09-12
+INV-00469,Taunggyi,Member,Food and beverages,42.36,4,8.47,2024-01-28
+INV-00673,Taunggyi,Member,Food and beverages,71.1,9,32.0,2024-08-21
+INV-00335,Taunggyi,Normal,Health and beauty,82.51,5,20.63,2024-12-21
+INV-00340,Bago,Member,Health and beauty,27.0,8,10.8,2024-04-30
+INV-00129,Bago,Member,Fashion accessories,87.6,8,35.04,2024-05-14
+INV-00043,Naypyitaw,Normal,Sports and travel,22.61,10,11.3,2024-09-16
+INV-00988,Yangon,Normal,Fashion accessories,58.1,10,29.05,2024-07-28
+INV-00329,Mandalay,Member,Fashion accessories,36.88,1,1.84,2024-12-17
+INV-00680,Mandalay,Normal,Home and lifestyle,28.59,1,1.43,2024-04-01
+INV-00441,Mandalay,Normal,Food and beverages,78.47,9,35.31,2024-04-27
+INV-00761,Mandalay,Member,Home and lifestyle,67.72,6,20.32,2024-10-19
+INV-00558,Yangon,Normal,Sports and travel,16.91,2,1.69,2024-05-05
+INV-00708,Taunggyi,Normal,Sports and travel,16.52,2,1.65,2024-07-17
+INV-00328,Mandalay,Normal,Home and lifestyle,55.58,3,8.34,2024-12-12
+INV-00058,Mandalay,Normal,Home and lifestyle,46.93,7,16.43,2024-12-05
+INV-00507,Mandalay,Member,Home and lifestyle,63.13,3,9.47,2024-03-01
+INV-00566,Naypyitaw,Normal,Food and beverages,13.72,8,5.49,2024-11-01
+INV-00497,Mandalay,Normal,Health and beauty,20.75,10,10.38,2024-06-02
+INV-00646,Mandalay,Member,Food and beverages,55.49,5,13.87,2024-10-05
+INV-00583,Bago,Member,Health and beauty,21.51,9,9.68,2024-05-03
+INV-00429,Naypyitaw,Member,Home and lifestyle,43.39,10,21.7,2024-01-18
+INV-00478,Yangon,Normal,Health and beauty,60.12,5,15.03,2024-02-10
+INV-00167,Mandalay,Member,Home and lifestyle,82.76,3,12.41,2024-09-24
+INV-00253,Mandalay,Member,Fashion accessories,83.0,10,41.5,2024-08-19
+INV-00736,Mandalay,Normal,Electronic accessories,27.02,1,1.35,2024-01-19
+INV-00338,Taunggyi,Member,Food and beverages,53.14,4,10.63,2024-09-02
+INV-00636,Taunggyi,Member,Health and beauty,58.99,6,17.7,2024-12-27
+INV-00539,Yangon,Normal,Home and lifestyle,67.64,10,33.82,2024-09-09
+INV-00990,Taunggyi,Normal,Home and lifestyle,49.24,10,24.62,2024-07-05
+INV-00270,Mandalay,Normal,Health and beauty,80.54,10,40.27,2024-05-22
+INV-00641,Bago,Member,Fashion accessories,77.45,9,34.85,2024-12-04
+INV-00009,Taunggyi,Normal,Electronic accessories,51.17,6,15.35,2024-06-18
+INV-00731,Mandalay,Member,Sports and travel,90.43,10,45.22,2024-10-19
+INV-00273,Taunggyi,Normal,Home and lifestyle,69.93,7,24.48,2024-04-15
+INV-00480,Bago,Member,Home and lifestyle,98.25,7,34.39,2024-01-26
+INV-00251,Bago,Normal,Electronic accessories,99.41,7,34.79,2024-05-19
+INV-00025,Bago,Normal,Electronic accessories,41.1,4,8.22,2024-03-07
+INV-00174,Yangon,Normal,Health and beauty,34.84,1,1.74,2024-05-26
+INV-00653,Mandalay,Normal,Food and beverages,67.06,7,23.47,2024-07-06
+INV-00848,Mandalay,Normal,Health and beauty,54.74,2,5.47,2024-11-26
+INV-00072,Yangon,Normal,Home and lifestyle,41.77,4,8.35,2024-10-04
+INV-00083,Taunggyi,Member,Health and beauty,75.19,1,3.76,2024-10-16
+INV-00432,Bago,Normal,Electronic accessories,99.9,2,9.99,2024-07-15
+INV-00291,Yangon,Normal,Electronic accessories,90.16,9,40.57,2024-07-17
+INV-00383,Mandalay,Member,Sports and travel,54.68,8,21.87,2024-02-02
+INV-00665,Taunggyi,Normal,Home and lifestyle,28.52,7,9.98,2024-08-28
+INV-00866,Yangon,Normal,Food and beverages,28.49,6,8.55,2024-10-24
+INV-00816,Yangon,Member,Food and beverages,26.76,3,4.01,2024-11-20
+INV-00123,Mandalay,Member,Fashion accessories,72.95,10,36.48,2024-04-25
+INV-00368,Taunggyi,Normal,Home and lifestyle,47.26,7,16.54,2024-02-15
+INV-00972,Yangon,Member,Electronic accessories,53.66,10,26.83,2024-08-12
+INV-00159,Taunggyi,Normal,Electronic accessories,68.15,5,17.04,2024-09-22
+INV-00229,Mandalay,Member,Fashion accessories,22.49,6,6.75,2024-09-25
+INV-00537,Taunggyi,Member,Food and beverages,44.53,7,15.59,2024-01-02
+INV-00017,Bago,Member,Fashion accessories,94.36,8,37.74,2024-06-23
+INV-00055,Mandalay,Member,Health and beauty,13.54,2,1.35,2024-02-20
+INV-00136,Naypyitaw,Normal,Fashion accessories,37.46,6,11.24,2024-04-26
+INV-00530,Mandalay,Member,Electronic accessories,53.91,8,21.56,2024-10-28
+INV-00213,Naypyitaw,Member,Sports and travel,55.58,7,19.45,2024-03-27
+INV-00957,Naypyitaw,Normal,Fashion accessories,90.07,7,31.52,2024-07-02
+INV-00712,Bago,Member,Sports and travel,26.75,1,1.34,2024-07-25
+INV-00958,Yangon,Normal,Home and lifestyle,88.95,2,8.9,2024-08-05
+INV-00419,Bago,Normal,Sports and travel,54.12,7,18.94,2024-05-23
+INV-00571,Yangon,Member,Electronic accessories,91.72,7,32.1,2024-03-08
+INV-00317,Yangon,Normal,Food and beverages,97.34,10,48.67,2024-07-22
+INV-00831,Yangon,Member,Electronic accessories,68.93,3,10.34,2024-09-10
+INV-00860,Mandalay,Member,Fashion accessories,14.26,7,4.99,2024-05-07
+INV-00104,Mandalay,Member,Home and lifestyle,43.74,2,4.37,2024-08-10
+INV-00493,Naypyitaw,Member,Electronic accessories,98.91,8,39.56,2024-10-18
+INV-00359,Yangon,Normal,Food and beverages,21.15,3,3.17,2024-05-11
+INV-00361,Yangon,Member,Home and lifestyle,37.69,7,13.19,2024-11-26
+INV-00152,Naypyitaw,Member,Fashion accessories,93.71,5,23.43,2024-11-04
+INV-00358,Yangon,Member,Food and beverages,92.47,6,27.74,2024-06-28
+INV-00103,Bago,Normal,Fashion accessories,39.41,8,15.76,2024-11-15
+INV-00209,Bago,Normal,Fashion accessories,35.48,10,17.74,2024-05-10
+INV-00343,Yangon,Normal,Sports and travel,34.26,3,5.14,2024-08-29
+INV-00635,Bago,Normal,Home and lifestyle,64.38,3,9.66,2024-01-10
+INV-00084,Bago,Normal,Home and lifestyle,37.6,3,5.64,2024-03-02
+INV-00003,Bago,Member,Health and beauty,43.51,9,19.58,2024-08-10
+INV-00454,Taunggyi,Normal,Sports and travel,33.62,3,5.04,2024-09-09
+INV-00606,Taunggyi,Member,Food and beverages,81.85,3,12.28,2024-11-15
+INV-00686,Yangon,Normal,Home and lifestyle,63.93,3,9.59,2024-10-25
+INV-00511,Mandalay,Member,Fashion accessories,85.1,8,34.04,2024-08-30
+INV-00246,Naypyitaw,Member,Sports and travel,15.96,2,1.6,2024-03-22
+INV-00390,Taunggyi,Normal,Health and beauty,31.89,7,11.16,2024-05-23
+INV-00876,Bago,Member,Electronic accessories,27.26,7,9.54,2024-12-19
+INV-00620,Bago,Normal,Food and beverages,30.12,5,7.53,2024-11-17
+INV-00060,Yangon,Member,Health and beauty,57.02,3,8.55,2024-08-13
+INV-00776,Yangon,Normal,Food and beverages,56.89,7,19.91,2024-03-14
+INV-00699,Yangon,Member,Health and beauty,58.12,8,23.25,2024-01-12
+INV-00923,Naypyitaw,Normal,Fashion accessories,35.74,8,14.3,2024-05-29
+INV-00492,Taunggyi,Normal,Food and beverages,72.87,9,32.79,2024-02-20
+INV-00489,Naypyitaw,Member,Fashion accessories,14.48,5,3.62,2024-07-23
+INV-00332,Taunggyi,Member,Electronic accessories,20.61,10,10.3,2024-01-31
+INV-00028,Mandalay,Normal,Food and beverages,26.82,2,2.68,2024-06-17
+INV-00555,Mandalay,Normal,Food and beverages,28.48,7,9.97,2024-10-17
+INV-00739,Naypyitaw,Normal,Sports and travel,84.81,1,4.24,2024-04-14
+INV-00423,Mandalay,Normal,Health and beauty,29.96,9,13.48,2024-08-17
+INV-00946,Yangon,Normal,Food and beverages,39.8,8,15.92,2024-09-29
+INV-00233,Bago,Normal,Home and lifestyle,26.49,4,5.3,2024-09-02
+INV-00256,Naypyitaw,Member,Home and lifestyle,67.02,9,30.16,2024-08-10
+INV-00603,Taunggyi,Member,Fashion accessories,28.13,6,8.44,2024-08-29
+INV-00373,Yangon,Member,Fashion accessories,47.79,10,23.89,2024-03-06
+INV-00880,Taunggyi,Member,Fashion accessories,85.46,2,8.55,2024-03-17
+INV-00226,Mandalay,Normal,Home and lifestyle,11.75,4,2.35,2024-10-28
+INV-00996,Bago,Normal,Fashion accessories,57.19,9,25.74,2024-07-31
+INV-00577,Naypyitaw,Member,Fashion accessories,20.43,6,6.13,2024-10-29
+INV-00330,Bago,Member,Health and beauty,35.35,8,14.14,2024-10-18
+INV-00477,Naypyitaw,Normal,Home and lifestyle,35.05,7,12.27,2024-01-30
+INV-00817,Mandalay,Member,Fashion accessories,92.87,6,27.86,2024-05-18
+INV-00338,Taunggyi,Member,Food and beverages,53.14,4,10.63,2024-09-02
+INV-00642,Yangon,Normal,Food and beverages,35.38,5,8.85,2024-05-17
+INV-00049,Yangon,Normal,Sports and travel,35.85,4,7.17,2024-08-17
+INV-00134,Mandalay,Member,Fashion accessories,14.3,1,0.72,2024-08-22
+INV-00092,Naypyitaw,Member,Electronic accessories,85.38,10,42.69,2024-01-10
+INV-00683,Taunggyi,Member,Electronic accessories,49.8,7,17.43,2024-05-09
+INV-00570,Yangon,Normal,Electronic accessories,72.49,1,3.62,2024-12-02
+INV-00669,Naypyitaw,Member,Food and beverages,32.58,3,4.89,2024-04-27
+INV-00654,Taunggyi,Normal,Home and lifestyle,17.8,7,6.23,2024-07-11
+INV-00697,Naypyitaw,Normal,Fashion accessories,26.52,2,2.65,2024-08-27
+INV-00953,Naypyitaw,Member,Electronic accessories,20.35,4,4.07,2024-09-23
+INV-00967,Taunggyi,Member,Sports and travel,45.64,7,15.97,2024-11-19
+INV-00105,Yangon,Member,Electronic accessories,65.13,8,26.05,2024-10-18
+INV-00222,Naypyitaw,Member,Fashion accessories,13.31,7,4.66,2024-10-25
+INV-00651,Yangon,Member,Home and lifestyle,33.66,6,10.1,2024-09-29
+INV-00895,Yangon,Normal,Health and beauty,98.97,5,24.74,2024-06-01
+INV-00063,Mandalay,Normal,Health and beauty,54.67,1,2.73,2024-05-03
+INV-00242,Yangon,Normal,Food and beverages,92.29,8,36.92,2024-10-09
+INV-00746,Naypyitaw,Normal,Home and lifestyle,56.72,7,19.85,2024-10-17
+INV-00490,Taunggyi,Normal,Fashion accessories,93.96,8,37.58,2024-03-07
+INV-00482,Bago,Member,Home and lifestyle,73.7,2,7.37,2024-09-19
+INV-00028,Mandalay,Normal,Food and beverages,26.82,2,2.68,2024-06-17
+INV-00002,Taunggyi,Normal,Food and beverages,63.89,10,31.95,2024-01-10
+INV-00801,Mandalay,Member,Home and lifestyle,27.41,5,6.85,2024-10-06
+INV-00596,Bago,Normal,Home and lifestyle,17.0,5,4.25,2024-01-13
+INV-00278,Bago,Member,Food and beverages,97.11,4,19.42,2024-04-22
+INV-00188,Yangon,Normal,Health and beauty,39.7,2,3.97,2024-04-28
+INV-00461,Mandalay,Normal,Food and beverages,54.69,8,21.88,2024-11-30
+INV-00973,Yangon,Normal,Food and beverages,40.6,5,10.15,2024-05-04
+INV-00601,Taunggyi,Member,Health and beauty,45.48,3,6.82,2024-05-17
+INV-00562,Mandalay,Member,Sports and travel,72.09,3,10.81,2024-01-24
+INV-00717,Mandalay,Member,Fashion accessories,58.6,1,2.93,2024-10-01
+INV-00447,Bago,Normal,Fashion accessories,66.55,9,29.95,2024-10-08
+INV-00922,Taunggyi,Normal,Food and beverages,79.97,8,31.99,2024-01-02
+INV-00572,Mandalay,Member,Fashion accessories,50.13,1,2.51,2024-11-10
+INV-00385,Mandalay,Member,Food and beverages,61.42,6,18.43,2024-06-28
+INV-00089,Bago,Member,Health and beauty,42.35,2,4.24,2024-12-08
+INV-00745,Bago,Normal,Sports and travel,42.07,8,16.83,2024-03-09
+INV-00013,Naypyitaw,Member,Fashion accessories,64.17,2,6.42,2024-07-02
+INV-00245,Naypyitaw,Normal,Home and lifestyle,45.73,6,13.72,2024-11-07
+INV-00980,Bago,Normal,Electronic accessories,67.27,4,13.45,2024-04-25
+INV-00251,Bago,Normal,Electronic accessories,99.41,7,34.79,2024-05-19
+INV-00622,Mandalay,Normal,Health and beauty,15.12,9,6.8,2024-11-16
+INV-00211,Taunggyi,Normal,Electronic accessories,14.33,2,1.43,2024-03-30
+INV-00839,Naypyitaw,Member,Sports and travel,70.47,5,17.62,2024-12-17
+INV-00819,Taunggyi,Member,Food and beverages,11.28,3,1.69,2024-07-07
+INV-00242,Yangon,Normal,Food and beverages,92.29,8,36.92,2024-10-09
+INV-00549,Naypyitaw,Member,Electronic accessories,29.73,7,10.41,2024-12-12
+INV-00560,Mandalay,Normal,Food and beverages,26.95,7,9.43,2024-11-21
+INV-00826,Taunggyi,Normal,Health and beauty,68.48,10,34.24,2024-01-05
+INV-00915,Naypyitaw,Normal,Home and lifestyle,57.46,10,28.73,2024-05-19
+INV-00418,Naypyitaw,Member,Electronic accessories,56.18,5,14.04,2024-04-07
+INV-00445,Bago,Normal,Food and beverages,14.19,10,7.1,2024-07-08
+INV-00144,Bago,Normal,Home and lifestyle,33.29,6,9.99,2024-07-15
+INV-00377,Bago,Normal,Sports and travel,68.02,10,34.01,2024-04-15
+INV-00633,Bago,Member,Home and lifestyle,23.79,7,8.33,2024-02-27
+INV-00395,Naypyitaw,Member,Fashion accessories,16.86,6,5.06,2024-06-06
+INV-00157,Yangon,Member,Health and beauty,17.52,1,0.88,2024-08-22
+INV-00440,Taunggyi,Member,Home and lifestyle,57.79,4,11.56,2024-11-21
+INV-00398,Taunggyi,Member,Electronic accessories,66.43,9,29.89,2024-04-17
+INV-00941,Yangon,Member,Home and lifestyle,57.17,9,25.73,2024-07-03
+INV-00613,Bago,Normal,Fashion accessories,95.22,8,38.09,2024-12-09
+INV-00528,Bago,Normal,Food and beverages,44.78,7,15.67,2024-08-11
+INV-00505,Naypyitaw,Member,Sports and travel,30.41,2,3.04,2024-05-18
+INV-00436,Bago,Member,Health and beauty,14.37,3,2.16,2024-11-24
+INV-00061,Yangon,Member,Health and beauty,16.8,2,1.68,2024-08-24
+INV-00204,Taunggyi,Normal,Sports and travel,93.35,9,42.01,2024-12-24
+INV-00074,Bago,Normal,Sports and travel,55.29,3,8.29,2024-02-25
+INV-00758,Mandalay,Normal,Fashion accessories,66.1,10,33.05,2024-03-24
+INV-00822,Naypyitaw,Normal,Food and beverages,77.54,6,23.26,2024-10-29
+INV-00019,Taunggyi,Normal,Home and lifestyle,94.17,7,32.96,2024-05-13
+INV-00165,Bago,Member,Food and beverages,51.09,6,15.33,2024-01-18
+INV-00428,Taunggyi,Member,Fashion accessories,97.05,1,4.85,2024-03-07
+INV-00114,Naypyitaw,Normal,Food and beverages,40.4,5,10.1,2024-07-02
+INV-00119,Naypyitaw,Member,Electronic accessories,92.09,8,36.84,2024-09-25
+INV-00630,Mandalay,Member,Food and beverages,53.75,3,8.06,2024-07-27
+INV-00711,Taunggyi,Normal,Health and beauty,29.97,6,8.99,2024-07-31
+INV-00457,Yangon,Normal,Home and lifestyle,69.51,3,10.43,2024-03-08
+INV-00293,Bago,Member,Sports and travel,53.69,1,2.68,2024-05-07
+INV-00146,Yangon,Member,Electronic accessories,83.31,10,41.66,2024-01-29
+INV-00867,Naypyitaw,Member,Electronic accessories,15.65,3,2.35,2024-12-14
+INV-00790,Yangon,Normal,Fashion accessories,33.98,6,10.19,2024-09-28
+INV-00940,Mandalay,Normal,Electronic accessories,74.27,4,14.85,2024-10-31
+INV-00138,Taunggyi,Member,Sports and travel,20.38,6,6.11,2024-04-06
+INV-00909,Bago,Member,Sports and travel,12.14,6,3.64,2024-12-20
+INV-00579,Naypyitaw,Member,Sports and travel,60.63,6,18.19,2024-12-07
+INV-00012,Yangon,Normal,Sports and travel,54.54,6,16.36,2024-08-10
+INV-00056,Naypyitaw,Normal,Home and lifestyle,65.42,9,29.44,2024-12-08
+INV-00977,Naypyitaw,Normal,Fashion accessories,73.03,10,36.52,2024-01-23
+INV-00758,Mandalay,Normal,Fashion accessories,66.1,10,33.05,2024-03-24
+INV-00902,Mandalay,Member,Sports and travel,43.59,1,2.18,2024-06-30
+INV-00674,Taunggyi,Member,Sports and travel,13.03,10,6.51,2024-12-13
+INV-00523,Mandalay,Member,Fashion accessories,23.76,4,4.75,2024-03-12
+INV-00706,Bago,Member,Home and lifestyle,63.98,4,12.8,2024-05-03
+INV-00738,Mandalay,Member,Home and lifestyle,36.47,1,1.82,2024-02-27
+INV-00881,Bago,Member,Fashion accessories,65.46,1,3.27,2024-11-19
+INV-00916,Yangon,Normal,Sports and travel,65.09,2,6.51,2024-03-07
+INV-00573,Bago,Member,Fashion accessories,69.35,7,24.27,2024-11-03
+INV-00628,Yangon,Normal,Fashion accessories,20.53,8,8.21,2024-09-29
+INV-00320,Naypyitaw,Member,Electronic accessories,98.26,8,39.3,2024-05-01
+INV-00294,Bago,Member,Sports and travel,56.61,8,22.64,2024-12-24
+INV-00757,Mandalay,Normal,Health and beauty,12.53,2,1.25,2024-01-18
+INV-00546,Taunggyi,Member,Food and beverages,47.94,8,19.18,2024-12-10
+INV-00968,Naypyitaw,Member,Home and lifestyle,33.97,6,10.19,2024-10-28
+INV-00521,Naypyitaw,Normal,Sports and travel,62.41,5,15.6,2024-03-10
+INV-00531,Bago,Normal,Electronic accessories,69.43,6,20.83,2024-05-10
+INV-00919,Naypyitaw,Normal,Home and lifestyle,65.01,2,6.5,2024-08-15
+INV-00218,Mandalay,Member,Fashion accessories,43.98,10,21.99,2024-12-10
+INV-00868,Bago,Member,Home and lifestyle,69.3,10,34.65,2024-03-02
+INV-00366,Yangon,Member,Electronic accessories,45.07,4,9.01,2024-11-18
+INV-00224,Yangon,Member,Health and beauty,61.0,3,9.15,2024-12-19
+INV-00602,Bago,Member,Health and beauty,74.81,8,29.92,2024-07-09
+INV-00965,Naypyitaw,Member,Electronic accessories,57.61,6,17.28,2024-02-13
+INV-00345,Yangon,Normal,Sports and travel,33.5,4,6.7,2024-08-07
+INV-00910,Mandalay,Member,Health and beauty,36.38,6,10.91,2024-04-25
+INV-00387,Bago,Normal,Home and lifestyle,16.69,2,1.67,2024-10-20
+INV-00529,Taunggyi,Normal,Food and beverages,89.85,5,22.46,2024-05-10
+INV-00353,Bago,Normal,Electronic accessories,19.27,2,1.93,2024-11-13
+INV-00536,Bago,Member,Food and beverages,58.31,1,2.92,2024-10-23
+INV-00210,Bago,Normal,Sports and travel,67.15,3,10.07,2024-06-06
+INV-00956,Yangon,Member,Fashion accessories,47.49,3,7.12,2024-12-15
+INV-00928,Mandalay,Normal,Electronic accessories,90.57,1,4.53,2024-02-03
+INV-00255,Bago,Normal,Electronic accessories,41.17,8,16.47,2024-09-24
+INV-00132,Mandalay,Normal,Electronic accessories,23.42,10,11.71,2024-10-24
+INV-00268,Bago,Member,Home and lifestyle,54.32,6,16.3,2024-11-13
+INV-00828,Yangon,Member,Health and beauty,93.09,9,41.89,2024-11-14
+INV-00182,Yangon,Normal,Fashion accessories,21.25,8,8.5,2024-07-30
+INV-00897,Bago,Normal,Home and lifestyle,59.92,3,8.99,2024-08-24
+INV-00375,Mandalay,Member,Home and lifestyle,52.57,9,23.66,2024-05-04
+INV-00062,Taunggyi,Normal,Electronic accessories,66.62,5,16.66,2024-12-19
+INV-00307,Taunggyi,Member,Food and beverages,37.41,9,16.83,2024-06-01
+INV-00857,Yangon,Normal,Sports and travel,55.45,5,13.86,2024-10-08
+INV-00892,Bago,Normal,Health and beauty,84.81,7,29.68,2024-09-20
+INV-00264,Yangon,Member,Health and beauty,37.13,2,3.71,2024-04-02
+INV-00645,Yangon,Member,Fashion accessories,56.58,4,11.32,2024-05-03
+INV-00735,Yangon,Member,Home and lifestyle,89.95,10,44.98,2024-10-07
+INV-00208,Naypyitaw,Member,Electronic accessories,47.04,6,14.11,2024-12-03
+INV-00487,Mandalay,Member,Home and lifestyle,77.45,6,23.24,2024-10-26
+INV-00444,Taunggyi,Member,Home and lifestyle,97.97,5,24.49,2024-03-20
+INV-00135,Bago,Member,Health and beauty,74.07,5,18.52,2024-03-03
+INV-00505,Naypyitaw,Member,Sports and travel,30.41,2,3.04,2024-05-18
+INV-00488,Taunggyi,Normal,Health and beauty,20.0,2,2.0,2024-08-04
+INV-00800,Taunggyi,Normal,Sports and travel,88.93,8,35.57,2024-01-06
+INV-00085,Yangon,Normal,Home and lifestyle,23.3,4,4.66,2024-01-15
+INV-00668,Mandalay,Normal,Food and beverages,97.73,2,9.77,2024-11-01
+INV-00671,Taunggyi,Member,Electronic accessories,94.43,3,14.16,2024-08-07
+INV-00266,Yangon,Member,Health and beauty,28.58,9,12.86,2024-11-19
+INV-00621,Yangon,Normal,Health and beauty,21.26,6,6.38,2024-05-09
+INV-00907,Taunggyi,Member,Sports and travel,34.51,6,10.35,2024-07-07
+INV-00896,Bago,Normal,Food and beverages,65.36,3,9.8,2024-06-20
+INV-00870,Mandalay,Normal,Electronic accessories,18.5,6,5.55,2024-07-31
+INV-00352,Yangon,Member,Home and lifestyle,40.13,9,18.06,2024-05-27
+INV-00216,Yangon,Normal,Fashion accessories,82.72,6,24.82,2024-02-02
+INV-00649,Mandalay,Member,Fashion accessories,53.93,1,2.7,2024-03-01
+INV-00840,Naypyitaw,Normal,Sports and travel,30.68,10,15.34,2024-12-08
+INV-00664,Mandalay,Normal,Home and lifestyle,76.45,3,11.47,2024-04-15
+INV-00214,Mandalay,Normal,Fashion accessories,93.97,5,23.49,2024-07-18
+INV-00369,Bago,Member,Sports and travel,45.98,3,6.9,2024-03-26
+INV-00837,Yangon,Normal,Health and beauty,67.5,3,10.12,2024-02-16
+INV-00927,Taunggyi,Member,Home and lifestyle,24.71,2,2.47,2024-02-16
+INV-00196,Naypyitaw,Normal,Home and lifestyle,22.65,5,5.66,2024-07-04
+INV-00234,Taunggyi,Member,Sports and travel,54.67,6,16.4,2024-03-02
+INV-00563,Taunggyi,Member,Electronic accessories,27.2,5,6.8,2024-02-14
+INV-00295,Yangon,Normal,Health and beauty,77.89,6,23.37,2024-09-14
+INV-00509,Naypyitaw,Member,Electronic accessories,36.38,1,1.82,2024-06-20
+INV-00365,Bago,Normal,Health and beauty,53.57,1,2.68,2024-07-04
+INV-00180,Mandalay,Member,Sports and travel,89.6,8,35.84,2024-09-19
+INV-00925,Taunggyi,Normal,Home and lifestyle,88.54,3,13.28,2024-10-01
+INV-00614,Taunggyi,Member,Health and beauty,66.5,10,33.25,2024-01-16
+INV-00475,Taunggyi,Member,Fashion accessories,54.3,7,19.0,2024-10-27
+INV-00553,Taunggyi,Normal,Fashion accessories,57.32,6,17.2,2024-01-15
+INV-00914,Bago,Member,Home and lifestyle,91.53,1,4.58,2024-05-28
+INV-00318,Taunggyi,Normal,Health and beauty,99.52,8,39.81,2024-01-19
+INV-00611,Bago,Normal,Electronic accessories,55.76,4,11.15,2024-09-19
+INV-00768,Bago,Normal,Sports and travel,76.18,1,3.81,2024-08-12
+INV-00912,Naypyitaw,Member,Home and lifestyle,30.5,9,13.73,2024-06-13
+INV-00994,Naypyitaw,Normal,Health and beauty,21.9,7,7.66,2024-03-01
+INV-00625,Taunggyi,Normal,Fashion accessories,90.13,8,36.05,2024-10-17
+INV-00420,Taunggyi,Member,Electronic accessories,81.2,7,28.42,2024-06-11
+INV-00363,Naypyitaw,Normal,Health and beauty,69.97,2,7.0,2024-05-10
+INV-00951,Yangon,Normal,Fashion accessories,16.0,2,1.6,2024-08-31
+INV-00662,Naypyitaw,Normal,Fashion accessories,96.85,4,19.37,2024-06-04
+INV-00076,Mandalay,Normal,Home and lifestyle,54.5,7,19.07,2024-02-22
+INV-00983,Mandalay,Member,Home and lifestyle,53.06,6,15.92,2024-11-21
+INV-00580,Naypyitaw,Normal,Sports and travel,49.01,2,4.9,2024-11-25
+INV-00230,Yangon,Member,Health and beauty,57.89,3,8.68,2024-02-28
+INV-00238,Taunggyi,Member,Fashion accessories,98.01,9,44.1,2024-12-18
+INV-00452,Bago,Member,Fashion accessories,61.3,3,9.19,2024-09-13
+INV-00351,Yangon,Member,Electronic accessories,14.92,8,5.97,2024-05-30
+INV-00634,Yangon,Member,Food and beverages,73.72,9,33.17,2024-02-13
+INV-00082,Mandalay,Normal,Food and beverages,91.56,4,18.31,2024-10-11
+INV-00453,Mandalay,Normal,Sports and travel,74.07,10,37.03,2024-11-04
+INV-00508,Mandalay,Normal,Health and beauty,91.99,5,23.0,2024-03-03
+INV-00289,Taunggyi,Member,Health and beauty,47.42,7,16.6,2024-08-24
+INV-00465,Mandalay,Member,Home and lifestyle,53.34,2,5.33,2024-01-28
+INV-00691,Naypyitaw,Normal,Sports and travel,54.57,4,10.91,2024-04-30
+INV-00506,Bago,Member,Food and beverages,69.82,9,31.42,2024-04-14
+INV-00617,Mandalay,Normal,Health and beauty,68.06,8,27.22,2024-11-16
+INV-00392,Taunggyi,Member,Home and lifestyle,67.31,6,20.19,2024-06-11
+INV-00323,Bago,Normal,Health and beauty,12.61,6,3.78,2024-03-21
+INV-00438,Yangon,Member,Electronic accessories,11.47,1,0.57,2024-04-30
+INV-00061,Yangon,Member,Health and beauty,16.8,2,1.68,2024-08-24
+INV-00781,Mandalay,Normal,Electronic accessories,64.72,6,19.42,2024-05-11
+INV-00503,Naypyitaw,Member,Electronic accessories,15.62,5,3.9,2024-10-09
+INV-00619,Naypyitaw,Member,Electronic accessories,63.35,6,19.01,2024-12-30
+INV-00305,Yangon,Normal,Health and beauty,84.42,10,42.21,2024-03-21
+INV-00565,Taunggyi,Member,Fashion accessories,42.46,5,10.62,2024-06-01
+INV-00393,Mandalay,Member,Electronic accessories,18.56,4,3.71,2024-11-21
+INV-00510,Yangon,Normal,Food and beverages,27.75,6,8.33,2024-06-11
+INV-00567,Taunggyi,Normal,Home and lifestyle,48.07,10,24.04,2024-10-07
+INV-00242,Yangon,Normal,Food and beverages,92.29,8,36.92,2024-10-09
+INV-00169,Mandalay,Normal,Fashion accessories,21.69,10,10.85,2024-08-06
+INV-00424,Bago,Member,Fashion accessories,92.34,3,13.85,2024-08-27
+INV-00515,Yangon,Normal,Sports and travel,62.61,6,18.78,2024-04-01
+INV-00121,Taunggyi,Member,Sports and travel,33.8,8,13.52,2024-06-07
+INV-00292,Naypyitaw,Member,Food and beverages,52.45,4,10.49,2024-03-10
+INV-00249,Taunggyi,Normal,Sports and travel,33.78,5,8.45,2024-05-14
+INV-00032,Taunggyi,Normal,Home and lifestyle,28.67,3,4.3,2024-05-26
+INV-00809,Naypyitaw,Normal,Fashion accessories,44.27,2,4.43,2024-03-07
+INV-00797,Yangon,Member,Sports and travel,30.88,9,13.9,2024-09-02
+INV-00744,Taunggyi,Normal,Electronic accessories,22.01,1,1.1,2024-12-03
+INV-00626,Mandalay,Normal,Electronic accessories,38.68,9,17.41,2024-03-01
+INV-00190,Bago,Normal,Fashion accessories,29.16,8,11.66,2024-07-05
+INV-00732,Naypyitaw,Member,Home and lifestyle,29.72,8,11.89,2024-06-22
+INV-00036,Naypyitaw,Member,Home and lifestyle,72.97,4,14.59,2024-04-06
+INV-00413,Taunggyi,Normal,Food and beverages,94.15,10,47.08,2024-10-10
+INV-00040,Bago,Member,Home and lifestyle,37.87,4,7.57,2024-05-01
+INV-00309,Taunggyi,Member,Food and beverages,70.87,5,17.72,2024-02-11
+INV-00410,Bago,Normal,Sports and travel,35.5,3,5.33,2024-10-17
+INV-00930,Naypyitaw,Member,Home and lifestyle,38.74,3,5.81,2024-06-27
+INV-00676,Bago,Normal,Fashion accessories,26.5,4,5.3,2024-05-20
+INV-00597,Naypyitaw,Member,Home and lifestyle,83.64,10,41.82,2024-06-05
+INV-00818,Taunggyi,Member,Sports and travel,69.61,2,6.96,2024-02-06
+INV-00476,Taunggyi,Normal,Food and beverages,19.71,5,4.93,2024-06-03
+INV-00917,Mandalay,Normal,Fashion accessories,85.27,10,42.63,2024-09-18
+INV-00412,Taunggyi,Member,Fashion accessories,55.91,7,19.57,2024-01-06
+INV-00283,Naypyitaw,Member,Electronic accessories,81.61,2,8.16,2024-12-20
+INV-00610,Yangon,Member,Home and lifestyle,40.23,5,10.06,2024-11-23
+INV-00406,Taunggyi,Normal,Food and beverages,91.27,6,27.38,2024-10-04
+INV-00140,Taunggyi,Normal,Fashion accessories,35.37,9,15.92,2024-02-25
+INV-00498,Naypyitaw,Member,Sports and travel,30.64,10,15.32,2024-06-07
+INV-00813,Taunggyi,Normal,Food and beverages,88.22,3,13.23,2024-10-17
+INV-00254,Yangon,Member,Sports and travel,62.62,9,28.18,2024-09-03
+INV-00648,Yangon,Normal,Health and beauty,95.47,10,47.74,2024-01-15
+INV-00751,Mandalay,Member,Home and lifestyle,81.69,8,32.68,2024-01-16
+INV-00069,Mandalay,Normal,Food and beverages,10.93,1,0.55,2024-08-06
+INV-00981,Mandalay,Member,Health and beauty,47.06,7,16.47,2024-02-24
+INV-00473,Mandalay,Normal,Electronic accessories,69.44,9,31.25,2024-01-05
+INV-00236,Bago,Member,Sports and travel,50.48,4,10.1,2024-11-23
+INV-00288,Bago,Normal,Home and lifestyle,59.36,10,29.68,2024-05-13
+INV-00844,Bago,Member,Food and beverages,29.45,8,11.78,2024-03-17
+INV-00859,Bago,Member,Home and lifestyle,40.42,7,14.15,2024-04-25
+INV-00569,Taunggyi,Normal,Home and lifestyle,12.06,2,1.21,2024-12-03
+INV-00609,Taunggyi,Normal,Health and beauty,72.6,8,29.04,2024-05-31
+INV-00276,Yangon,Member,Food and beverages,72.27,1,3.61,2024-01-02
+INV-00743,Naypyitaw,Normal,Sports and travel,16.87,9,7.59,2024-09-12
+INV-00260,Yangon,Normal,Food and beverages,51.58,2,5.16,2024-01-14
+INV-00059,Bago,Normal,Health and beauty,63.16,4,12.63,2024-08-14
+INV-00525,Taunggyi,Normal,Food and beverages,62.04,4,12.41,2024-08-27
+INV-00591,Bago,Member,Food and beverages,10.72,1,0.54,2024-09-28
+INV-00838,Bago,Member,Fashion accessories,98.37,10,49.19,2024-01-06
+INV-00976,Taunggyi,Member,Fashion accessories,33.97,4,6.79,2024-08-14
+INV-00728,Yangon,Member,Food and beverages,71.57,8,28.63,2024-07-03
+INV-00703,Bago,Normal,Sports and travel,95.5,3,14.33,2024-01-01
+INV-00223,Taunggyi,Normal,Electronic accessories,78.18,9,35.18,2024-02-29
+INV-00037,Mandalay,Member,Home and lifestyle,25.29,8,10.12,2024-08-02
+INV-00865,Yangon,Normal,Fashion accessories,88.61,3,13.29,2024-05-24
+INV-00113,Mandalay,Normal,Health and beauty,59.53,10,29.77,2024-10-22
+INV-00290,Taunggyi,Member,Fashion accessories,46.09,7,16.13,2024-10-12
+INV-00162,Bago,Member,Sports and travel,99.68,6,29.9,2024-05-21
+INV-00287,Mandalay,Normal,Sports and travel,57.61,2,5.76,2024-03-25
+INV-00191,Yangon,Normal,Fashion accessories,51.24,8,20.5,2024-01-28
+INV-00586,Yangon,Member,Fashion accessories,56.46,8,22.58,2024-10-27
+INV-00535,Yangon,Normal,Health and beauty,79.47,3,11.92,2024-03-02
+INV-00187,Yangon,Member,Home and lifestyle,71.49,8,28.6,2024-01-09
+INV-00627,Bago,Normal,Electronic accessories,36.73,2,3.67,2024-01-09
+INV-00172,Mandalay,Member,Home and lifestyle,47.79,4,9.56,2024-01-12
+INV-00853,Yangon,Member,Sports and travel,64.04,9,28.82,2024-10-02
+INV-00785,Taunggyi,Member,Fashion accessories,51.08,5,12.77,2024-06-02
+INV-00773,Taunggyi,Normal,Food and beverages,73.94,5,18.48,2024-10-21
+INV-00026,Naypyitaw,Member,Food and beverages,32.43,2,3.24,2024-12-24
+INV-00007,Mandalay,Member,Food and beverages,94.77,3,14.22,2024-05-31
+INV-00887,Mandalay,Member,Food and beverages,98.78,7,34.57,2024-09-21
+INV-00483,Taunggyi,Normal,Health and beauty,51.84,4,10.37,2024-09-20
+INV-00400,Taunggyi,Normal,Electronic accessories,71.02,1,3.55,2024-11-13
+INV-00177,Mandalay,Member,Home and lifestyle,18.07,10,9.04,2024-10-18
+INV-00638,Bago,Member,Home and lifestyle,21.94,8,8.78,2024-01-08
+INV-00093,Taunggyi,Normal,Fashion accessories,90.66,6,27.2,2024-02-17
+INV-00604,Naypyitaw,Normal,Health and beauty,57.2,8,22.88,2024-04-01
+INV-00081,Taunggyi,Member,Home and lifestyle,63.91,8,25.56,2024-07-30
+INV-00106,Bago,Normal,Electronic accessories,18.46,5,4.62,2024-04-10
+INV-00241,Bago,Normal,Health and beauty,72.43,9,32.59,2024-12-30
+INV-00834,Naypyitaw,Member,Fashion accessories,91.88,7,32.16,2024-12-26
+INV-00189,Yangon,Member,Fashion accessories,88.98,8,35.59,2024-05-10
+INV-00083,Taunggyi,Member,Health and beauty,75.19,1,3.76,2024-10-16
+INV-00705,Mandalay,Member,Home and lifestyle,60.42,1,3.02,2024-08-31
+INV-00954,Mandalay,Normal,Home and lifestyle,70.79,3,10.62,2024-07-27
+INV-00716,Mandalay,Member,Fashion accessories,96.1,1,4.8,2024-02-20
+INV-00605,Bago,Normal,Health and beauty,15.12,7,5.29,2024-07-29
+INV-00724,Mandalay,Normal,Home and lifestyle,32.04,7,11.21,2024-03-13
+INV-00514,Mandalay,Normal,Health and beauty,17.7,9,7.96,2024-05-02
+INV-00349,Yangon,Member,Food and beverages,49.47,7,17.31,2024-07-21
+INV-00120,Yangon,Member,Home and lifestyle,31.91,5,7.98,2024-02-15
+INV-00313,Mandalay,Member,Fashion accessories,17.94,6,5.38,2024-05-18
+INV-00227,Naypyitaw,Member,Health and beauty,82.61,3,12.39,2024-10-04
+INV-00250,Bago,Member,Electronic accessories,22.04,5,5.51,2024-07-12
+INV-00632,Yangon,Normal,Sports and travel,65.37,9,29.42,2024-12-16
+INV-00339,Yangon,Normal,Food and beverages,86.51,4,17.3,2024-02-03
+INV-00020,Taunggyi,Member,Electronic accessories,45.81,1,2.29,2024-04-05
+INV-00554,Bago,Member,Sports and travel,79.24,5,19.81,2024-03-30
+INV-00450,Taunggyi,Member,Health and beauty,84.66,8,33.86,2024-10-09
+INV-00197,Naypyitaw,Member,Fashion accessories,71.17,9,32.03,2024-04-12
+INV-00685,Mandalay,Normal,Fashion accessories,17.21,9,7.74,2024-05-17
+INV-00921,Yangon,Normal,Food and beverages,31.21,1,1.56,2024-07-20
+INV-00545,Bago,Member,Sports and travel,27.58,10,13.79,2024-10-07
+INV-00297,Yangon,Normal,Sports and travel,22.16,6,6.65,2024-03-28
+INV-00396,Bago,Member,Sports and travel,29.1,9,13.1,2024-06-10
+INV-00281,Mandalay,Member,Health and beauty,23.86,3,3.58,2024-06-18
+INV-00225,Mandalay,Normal,Fashion accessories,47.45,2,4.75,2024-12-09
+INV-00195,Mandalay,Member,Home and lifestyle,69.22,8,27.69,2024-02-11
+INV-00354,Taunggyi,Member,Sports and travel,56.9,7,19.92,2024-06-09
+INV-00086,Yangon,Member,Health and beauty,25.5,8,10.2,2024-09-13
+INV-00176,Mandalay,Normal,Sports and travel,71.73,8,28.69,2024-10-23
+INV-00709,Taunggyi,Member,Sports and travel,87.0,10,43.5,2024-08-06
+INV-00598,Taunggyi,Member,Electronic accessories,76.09,7,26.63,2024-05-11
+INV-00843,Naypyitaw,Member,Home and lifestyle,54.48,4,10.9,2024-05-16
+INV-00750,Naypyitaw,Normal,Sports and travel,48.45,3,7.27,2024-10-19
+INV-00889,Mandalay,Normal,Home and lifestyle,59.65,9,26.84,2024-01-08
+INV-00877,Yangon,Member,Health and beauty,63.62,4,12.72,2024-07-05
+INV-00118,Taunggyi,Member,Electronic accessories,89.81,7,31.43,2024-08-21
+INV-00407,Mandalay,Normal,Fashion accessories,70.42,8,28.17,2024-06-17
+INV-00747,Yangon,Normal,Home and lifestyle,34.06,3,5.11,2024-04-29
+INV-00795,Naypyitaw,Normal,Fashion accessories,97.19,10,48.59,2024-09-24
+INV-00491,Naypyitaw,Normal,Health and beauty,93.42,5,23.36,2024-06-02
+INV-00898,Yangon,Normal,Home and lifestyle,87.17,7,30.51,2024-03-23
+INV-00846,Taunggyi,Normal,Food and beverages,44.2,10,22.1,2024-05-30
+INV-00784,Bago,Normal,Electronic accessories,70.19,8,28.08,2024-07-10
+INV-00615,Taunggyi,Member,Electronic accessories,60.47,7,21.16,2024-11-11
+INV-00151,Mandalay,Member,Health and beauty,14.22,2,1.42,2024-03-13
+INV-00279,Taunggyi,Member,Fashion accessories,69.46,8,27.78,2024-01-10
+INV-00376,Yangon,Member,Electronic accessories,70.22,6,21.07,2024-12-04
+INV-00823,Yangon,Member,Sports and travel,15.79,6,4.74,2024-09-08
+INV-00557,Mandalay,Member,Fashion accessories,84.49,10,42.25,2024-05-17
+INV-00766,Taunggyi,Normal,Fashion accessories,20.09,8,8.04,2024-10-07
+INV-00379,Yangon,Normal,Electronic accessories,40.85,7,14.3,2024-08-30
+INV-00277,Mandalay,Member,Food and beverages,87.03,10,43.52,2024-07-23
+INV-00547,Taunggyi,Normal,Electronic accessories,87.46,9,39.36,2024-11-18
+INV-00714,Taunggyi,Member,Fashion accessories,41.53,6,12.46,2024-06-08
+INV-00832,Taunggyi,Normal,Fashion accessories,63.35,3,9.5,2024-03-24
+INV-00460,Yangon,Member,Food and beverages,70.15,6,21.05,2024-03-25
+INV-00850,Mandalay,Member,Sports and travel,28.4,10,14.2,2024-02-15
+INV-00792,Bago,Normal,Electronic accessories,52.49,7,18.37,2024-09-16
+INV-00533,Yangon,Member,Electronic accessories,70.31,4,14.06,2024-08-26
+INV-00780,Taunggyi,Member,Food and beverages,21.39,1,1.07,2024-03-11
+INV-00650,Yangon,Normal,Electronic accessories,21.53,8,8.61,2024-05-12
+INV-00756,Bago,Normal,Health and beauty,97.08,9,43.69,2024-06-30
+INV-00235,Taunggyi,Member,Fashion accessories,86.52,3,12.98,2024-01-08
+INV-00723,Naypyitaw,Normal,Sports and travel,62.57,5,15.64,2024-02-07
+INV-00231,Naypyitaw,Member,Fashion accessories,60.55,7,21.19,2024-03-26
+INV-00380,Bago,Member,Sports and travel,88.04,7,30.81,2024-08-17
+INV-00164,Mandalay,Member,Health and beauty,18.72,3,2.81,2024-09-02
+INV-00386,Bago,Normal,Fashion accessories,15.13,2,1.51,2024-05-09
+INV-00779,Yangon,Member,Home and lifestyle,25.83,3,3.87,2024-02-14
+INV-00772,Bago,Normal,Electronic accessories,95.74,3,14.36,2024-12-29
+INV-00770,Mandalay,Member,Home and lifestyle,46.35,6,13.91,2024-01-18
+INV-00112,Mandalay,Normal,Fashion accessories,82.27,6,24.68,2024-04-26
+INV-00966,Mandalay,Member,Home and lifestyle,99.24,8,39.7,2024-04-29
+INV-00495,Naypyitaw,Normal,Health and beauty,86.31,1,4.32,2024-10-26
+INV-00952,Mandalay,Normal,Fashion accessories,61.07,5,15.27,2024-06-02
+INV-00955,Bago,Normal,Electronic accessories,11.45,4,2.29,2024-01-27
+INV-00502,Bago,Member,Food and beverages,45.71,6,13.71,2024-12-31
+INV-00575,Yangon,Normal,Food and beverages,15.31,9,6.89,2024-06-23
+INV-00903,Mandalay,Member,Health and beauty,91.34,9,41.1,2024-08-21
+INV-00004,Naypyitaw,Member,Electronic accessories,86.58,2,8.66,2024-07-11
+INV-00534,Naypyitaw,Normal,Home and lifestyle,38.19,4,7.64,2024-07-24
+INV-00327,Taunggyi,Member,Fashion accessories,80.02,2,8.0,2024-10-26
+INV-00524,Naypyitaw,Member,Food and beverages,75.36,8,30.14,2024-05-25
+INV-00693,Naypyitaw,Normal,Health and beauty,82.84,2,8.28,2024-10-27
+INV-00729,Mandalay,Member,Sports and travel,63.73,5,15.93,2024-10-17
+INV-00481,Yangon,Member,Food and beverages,60.58,10,30.29,2024-06-05
+INV-00066,Bago,Member,Home and lifestyle,69.12,9,31.1,2024-10-11
+INV-00149,Mandalay,Normal,Electronic accessories,12.8,3,1.92,2024-09-05
+INV-00415,Naypyitaw,Normal,Electronic accessories,50.99,4,10.2,2024-05-20
+INV-00791,Yangon,Member,Health and beauty,55.89,5,13.97,2024-01-02
+INV-00908,Yangon,Normal,Sports and travel,77.74,2,7.77,2024-02-14
+INV-00517,Taunggyi,Normal,Home and lifestyle,22.25,8,8.9,2024-07-08
+INV-00584,Mandalay,Normal,Electronic accessories,74.34,9,33.45,2024-12-22
+INV-00057,Mandalay,Normal,Food and beverages,45.81,3,6.87,2024-03-27
+INV-00471,Mandalay,Normal,Home and lifestyle,17.52,10,8.76,2024-04-04
+INV-00847,Naypyitaw,Member,Home and lifestyle,64.37,4,12.87,2024-05-19
+INV-00588,Naypyitaw,Normal,Home and lifestyle,76.95,3,11.54,2024-11-16
+INV-00841,Yangon,Member,Electronic accessories,93.15,7,32.6,2024-08-16
+INV-00594,Mandalay,Member,Sports and travel,80.15,9,36.07,2024-06-18
+INV-00324,Taunggyi,Normal,Sports and travel,22.95,8,9.18,2024-12-20
+INV-00389,Bago,Normal,Fashion accessories,19.08,1,0.95,2024-07-05
+INV-00200,Naypyitaw,Normal,Fashion accessories,95.78,2,9.58,2024-12-30
+INV-00612,Yangon,Member,Sports and travel,86.04,8,34.42,2024-08-08
diff --git a/docs/MakeFile b/docs/MakeFile
deleted file mode 100644
index 3d0ce63c6..000000000
--- a/docs/MakeFile
+++ /dev/null
@@ -1,23 +0,0 @@
-# Minimal makefile for Sphinx documentation
-
-# You can set these variables from the command line, and also
-# from the environment for the first two.
-SPHINXOPTS ?=
-SPHINXBUILD ?= sphinx-build
-SOURCEDIR = .
-BUILDDIR = _build
-
-# Put it first so that "make" without argument is like "make help".
-help:
- @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
-
-.PHONY: help Makefile clean
-
-# Custom clean command
-clean:
- rm -rf $(BUILDDIR)/*
-
-# Catch-all target: route all unknown targets to Sphinx using the new
-# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS).
-%: Makefile
- @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O)
\ No newline at end of file
diff --git a/docs/ai/index.md b/docs/ai/index.md
index 4ab6f7579..9e92748df 100644
--- a/docs/ai/index.md
+++ b/docs/ai/index.md
@@ -34,7 +34,7 @@ The **Agent variant** picker in settings selects execution mode:
- **Live (REPL)** *(default)* — each step applies immediately, the affected subgraph runs (Performance) or samples (Development), and the runtime observation feeds back to the model. Failed steps auto-undo and retry. No diff to accept — the canvas is the running record. Higher latency per step because each one does real work.
- **Staged** — small/local-model-friendly. A tightly-scoped state machine makes one decision per LLM round and bundles proposals into a `GraphDiff` for **Accept** (atomic, one undo point) or **Reject** with an optional note that becomes context for the next attempt.
-- **Single-shot full** — big-model mode (Sonnet, Opus, GPT-4.1, Gemini Pro). Exposes the full tool catalog in one call. Same staged-diff review flow.
+- **Single-shot full** — big-model mode (the provider's strongest tool-capable model for the `agent_complex` surface). Exposes the full tool catalog in one call. Same staged-diff review flow.
The **Verify plan completion** checkbox adds one extra LLM round after the agent decides it's done, to walk its plan as a checklist — catches multi-step plans that terminate after step 1.
diff --git a/docs/ai/providers.md b/docs/ai/providers.md
index d9bfddcb7..9b7238251 100644
--- a/docs/ai/providers.md
+++ b/docs/ai/providers.md
@@ -1,6 +1,6 @@
# Provider Setup (BYOK)
-The [AI Assistant](index.md) runs against any major LLM provider — pick the one that fits your budget, latency needs, or compliance posture. Today that's Anthropic, OpenAI, Google, Groq, OpenRouter, and a local Ollama server. You bring your own API key; Flowfile encrypts it at rest with Fernet (using `FLOWFILE_MASTER_KEY` / `master_key.txt`), the same scheme that protects your other secrets. For air-gapped work, Ollama lets the entire AI layer run on your laptop with no traffic leaving your machine.
+The [AI Assistant](index.md) runs against one of six LLM providers — pick the one that fits your budget, latency needs, or compliance posture: Anthropic, OpenAI, Google, Groq, OpenRouter, and a local Ollama server. You bring your own API key; Flowfile encrypts it at rest with Fernet (using `FLOWFILE_MASTER_KEY` / `master_key.txt`), the same scheme that protects your other secrets. For air-gapped work, Ollama lets the entire AI layer run on your laptop with no traffic leaving your machine.
!!! info "Not in Flowfile Lite"
The AI Assistant (and BYOK provider setup) requires the full desktop/server build, not the browser-only [Flowfile Lite](../users/deployment/lite.md) edition.
@@ -9,17 +9,22 @@ The [AI Assistant](index.md) runs against any major LLM provider — pick the on
## Supported providers
+Default models below reflect the provider classes as of 2026-07.
+
| Provider | Default model | Tools | Streaming | Key env var | Notes |
|----------|---------------|:-----:|:---------:|-------------|-------|
| **Anthropic** | `claude-sonnet-4-6` | ✓ | ✓ | `ANTHROPIC_API_KEY` | Best balance of quality and tool-use reliability. Haiku 4.5 is the default for fast surfaces (Cmd+K, ghost-node, autocomplete); Opus 4.7 for `agent_complex`. |
| **OpenAI** | `gpt-4.1-mini` | ✓ | ✓ | `OPENAI_API_KEY` | Mini tier for the cheap surfaces; full `gpt-4.1` for `explain` / `agent_complex` / `docgen`. Strict structured outputs supported via litellm. |
| **Google (Gemini)** | `gemini-2.5-flash` | ✓ | ✓ | `GEMINI_API_KEY` or `GOOGLE_API_KEY` | Generous free tier (~250–1000 req/day, no card). Pro for `agent_complex`. |
-| **Groq** | `qwen/qwen3-coder-30b-a3b-instruct` | ✓ | ✓ | `GROQ_API_KEY` | Very fast inference (~30 RPM free tier); good fit for low-TTFB surfaces (Cmd+K, ghost-node). |
-| **OpenRouter** | `qwen/qwen3-coder-30b-a3b-instruct` | ✓ | ✓ | `OPENROUTER_API_KEY` | Unified façade for 50+ models with a single key. The `agent_staged` default is `meta-llama/llama-3.3-70b-instruct` (free tier). |
+| **Groq** | `qwen/qwen3-32b` | ✓ | ✓ | `GROQ_API_KEY` | Very fast inference (~30 RPM free tier); good fit for low-TTFB surfaces (Cmd+K, ghost-node). |
+| **OpenRouter** | `qwen/qwen3-coder-30b-a3b-instruct` | ✓ | ✓ | `OPENROUTER_API_KEY` | One key routing to many hosted models. The `agent_staged` default is `meta-llama/llama-3.3-70b-instruct` (free tier). |
| **Ollama** | `llama3.1:8b` | ✓ (model-dependent) | ✓ | *(none — local)* | Self-hosted; talks to your local Ollama server (default `http://localhost:11434`). Tool-use works on Llama 3.1+ and most newer instruct models. |
The "Tools" column means the provider can return structured tool-call arguments — required for the Agent surface. The Agent refuses to start against a model that lacks tool support.
+!!! note "Model IDs drift with releases"
+ The default and per-surface model IDs are hardcoded in the provider classes at `flowfile_core/flowfile_core/ai/providers/*.py` (one file per provider) and are bumped as new models ship. The table above is a snapshot; check those files for the current values before relying on a specific model name.
+
---
## Configuring keys in the UI
@@ -29,7 +34,7 @@ The recommended path is the in-app settings panel:
1. Open **Settings → AI Providers**.
2. Pick a provider from the list. The panel shows class-level metadata (default model, supports tools, supports streaming) plus your current credential status: **Configured** (key saved), **Env fallback** (no key saved but a recognised env var is set on the server), or **Unconfigured**.
3. Paste the API key into the *API key* field and click **Save**. For Ollama or self-hosted endpoints, set *API base* to the server URL.
-4. Click **Test**. Flowfile issues a 1-token ping and records the result on the credential (`last_tested_at`, `last_test_status`). A green checkmark means you're good to go.
+4. Click **Test**. Flowfile issues a 1-token ping and records the result on the credential (`last_tested_at`, `last_test_status`).

@@ -57,7 +62,7 @@ If no credential row exists for a user, Flowfile falls back to the standard prov
## Choosing models per surface
-Each AI feature is a *surface* (`chat`, `explain`, `agent_staged`, `agent_complex`, `cmd_k`, `ghost_node`, `settings_autocomplete`, `lineage`, `docgen`, `intent_classifier`). Each provider class ships sensible per-surface defaults — Haiku for fast paths, Sonnet for thinking paths, etc. — so you usually don't need to override anything.
+Each AI feature is a *surface* (`chat`, `explain`, `agent_staged`, `agent_complex`, `cmd_k`, `ghost_node`, `settings_autocomplete`, `lineage`, `docgen`, `intent_classifier`, `cron`). Each provider class ships sensible per-surface defaults — Haiku for fast paths, Sonnet for thinking paths, etc. — so you usually don't need to override anything.
When you do want to override:
diff --git a/docs/assets/flows/sales_pipeline.yaml b/docs/assets/flows/sales_pipeline.yaml
new file mode 100644
index 000000000..e769fa97b
--- /dev/null
+++ b/docs/assets/flows/sales_pipeline.yaml
@@ -0,0 +1,111 @@
+flowfile_version: 0.8.2
+flowfile_id: 12
+flowfile_name: Sales Pipeline
+flowfile_settings:
+ description: null
+ execution_mode: Development
+ execution_location: local
+ auto_save: false
+ show_detailed_progress: true
+ max_parallel_workers: 4
+ source_registration_id: null
+ parameters: []
+nodes:
+- id: 1
+ type: read
+ is_start_node: true
+ description: ''
+ node_reference: null
+ x_position: 0
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: []
+ outputs: [2]
+ setting_input:
+ cache_results: false
+ received_file:
+ path: https://raw.githubusercontent.com/edwardvaneechoud/flowfile/main/data/templates/supermarket_sales.csv
+ file_type: csv
+ table_settings:
+ file_type: csv
+- id: 2
+ type: unique
+ is_start_node: false
+ description: ''
+ node_reference: null
+ x_position: 250
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [1]
+ outputs: [3]
+ setting_input:
+ cache_results: false
+ unique_input:
+ columns: null
+ strategy: any
+- id: 3
+ type: filter
+ is_start_node: false
+ description: ''
+ node_reference: null
+ x_position: 500
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [2]
+ outputs: [4]
+ setting_input:
+ cache_results: false
+ filter_input:
+ mode: advanced
+ advanced_filter: '[quantity] > 7'
+- id: 4
+ type: group_by
+ is_start_node: false
+ description: ''
+ node_reference: null
+ x_position: 750
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [3]
+ outputs: [5]
+ setting_input:
+ cache_results: false
+ groupby_input:
+ agg_cols:
+ - old_name: city
+ agg: groupby
+ new_name: city
+ - old_name: gross_income
+ agg: sum
+ new_name: total_income
+ - old_name: gross_income
+ agg: median
+ new_name: median_income
+- id: 5
+ type: explore_data
+ is_start_node: false
+ description: 'Explore the output!'
+ node_reference: null
+ x_position: 1000
+ y_position: 200
+ left_input_id: null
+ right_input_id: null
+ input_ids: [4]
+ outputs: []
+ setting_input:
+ cache_results: false
+ graphic_walker_input: null
+_template_meta:
+ template_id: sales_pipeline
+ name: 'Sales pipeline: clean, filter, aggregate'
+ description: Drop duplicate sales rows, keep high-quantity orders, then total and
+ median gross income per city.
+ category: Beginner
+ tags: [unique, filter, group_by]
+ node_count: 5
+ icon: insights
+_required_csv_files: [supermarket_sales.csv]
diff --git a/docs/assets/try-sales-pipeline.html b/docs/assets/try-sales-pipeline.html
new file mode 100644
index 000000000..7fd452648
--- /dev/null
+++ b/docs/assets/try-sales-pipeline.html
@@ -0,0 +1,11 @@
+
+
+
+
+Opening the sales pipeline in Flowfile Lite…
+
+
+
+
Opening the sales pipeline in Flowfile Lite… The flow definition travels in the link; its sample data is read from the public sample CSV on GitHub.
+
+
diff --git a/docs/community.md b/docs/community.md
index 072d66fa8..d7b7ace49 100644
--- a/docs/community.md
+++ b/docs/community.md
@@ -1,6 +1,6 @@
# Community
-Flowfile is an MIT-licensed open source project, and it's built by the people who use it. Whether you want to ask a question, share a flow, report a bug, or propose a feature, there's a place for you here.
+Flowfile is an MIT-licensed open source project. Use the channels below to ask a question, share a flow, report a bug, or propose a feature.
## Where to go
diff --git a/docs/conf.py b/docs/conf.py
deleted file mode 100644
index e890c1db2..000000000
--- a/docs/conf.py
+++ /dev/null
@@ -1,91 +0,0 @@
-# Configuration file for the Sphinx documentation builder.
-import os
-import sys
-
-sys.path.insert(0, os.path.abspath(".."))
-
-# Project information
-project = "Flowfile"
-copyright = "2024, Flowfile Contributors"
-author = "Edward van Eechoud"
-release = "0.1.2" # Update this with your version
-
-# Add any Sphinx extension module names here
-extensions = [
- "sphinx.ext.autodoc", # Core documentation engine
- "sphinx.ext.napoleon", # Support for Google-style docstrings
- "sphinx.ext.viewcode", # Add links to source code
- "sphinx.ext.githubpages", # Generate .nojekyll file
- "myst_parser", # Markdown support
- "sphinx_copybutton", # Add copy button to code blocks
- "sphinx.ext.intersphinx", # Link to other projects' documentation
- "sphinx_autodoc_typehints", # Better type hints support
- "autoapi.extension", # API documentation
-]
-
-autoapi_type = "python"
-autoapi_dirs = [
- "../flowfile_core",
- "../flowfile_worker",
-]
-autoapi_template_dir = "_templates/autoapi"
-autoapi_python_class_content = "both"
-
-# Add any paths that contain templates here
-templates_path = ["_templates"]
-exclude_patterns = ["_build", "Thumbs.db", ".DS_Store"]
-
-# HTML theme settings
-html_theme = "sphinx_rtd_theme"
-html_static_path = ["_static"]
-html_logo = "../.github/images/logo.png"
-html_favicon = "../.github/images/logo.png"
-
-# Theme options
-html_theme_options = {
- "logo_only": True,
- "display_version": True,
- "prev_next_buttons_location": "bottom",
- "style_external_links": False,
- "style_nav_header_background": "#2980B9",
- # Dark mode colors
- "dark_mode_theme": "dark",
- "dark_mode_css_variables": {
- "color-brand-primary": "#2980B9",
- "color-brand-content": "#2980B9",
- },
-}
-
-# These folders are copied to the documentation's HTML output
-html_static_path = ["_static"]
-
-# These paths are either relative to html_static_path or fully qualified paths
-html_css_files = [
- "css/custom.css",
-]
-
-# Intersphinx configuration
-intersphinx_mapping = {
- "python": ("https://docs.python.org/3", None),
- "polars": ("https://pola-rs.github.io/polars/py-polars/html/", None),
-}
-
-# MyST Markdown settings
-myst_enable_extensions = [
- "colon_fence",
- "deflist",
-]
-
-# Napoleon settings
-napoleon_google_docstring = True
-napoleon_numpy_docstring = True
-napoleon_include_init_with_doc = True
-napoleon_include_private_with_doc = True
-napoleon_include_special_with_doc = True
-napoleon_use_admonition_for_examples = True
-napoleon_use_admonition_for_notes = True
-napoleon_use_admonition_for_references = True
-napoleon_use_ivar = True
-napoleon_use_param = True
-napoleon_use_rtype = True
-napoleon_type_aliases = None
diff --git a/docs/examples/catalog_analysis.py b/docs/examples/catalog_analysis.py
new file mode 100644
index 000000000..cef9a3be8
--- /dev/null
+++ b/docs/examples/catalog_analysis.py
@@ -0,0 +1,38 @@
+"""Write an aggregate to the Flowfile catalog, then query it back with SQL."""
+
+# --8<-- [start:example]
+import flowfile as ff
+from flowfile_frame import read_catalog_sql
+
+SALES = "https://raw.githubusercontent.com/edwardvaneechoud/flowfile/main/data/templates/supermarket_sales.csv"
+
+income_by_city = (
+ ff.read_csv(SALES)
+ .group_by("city")
+ .agg(ff.col("gross_income").sum().alias("total_income"))
+)
+
+ff.write_catalog_table(
+ income_by_city, "docs_sales_by_city", schema=ff.default_schema(), write_mode="overwrite"
+)
+
+top_cities = read_catalog_sql(
+ "SELECT city, total_income FROM docs_sales_by_city ORDER BY total_income DESC"
+).collect()
+# --8<-- [end:example]
+
+assert top_cities.columns == ["city", "total_income"]
+assert top_cities.height == 5
+
+rows = top_cities.to_dicts()
+assert rows[0]["city"] == "Taunggyi"
+assert round(rows[0]["total_income"], 2) == 3525.60
+
+by_city = {row["city"]: round(row["total_income"], 2) for row in rows}
+assert by_city == {
+ "Bago": 3051.94,
+ "Mandalay": 3520.57,
+ "Naypyitaw": 2521.64,
+ "Taunggyi": 3525.60,
+ "Yangon": 3312.56,
+}
diff --git a/docs/examples/catalog_references.py b/docs/examples/catalog_references.py
new file mode 100644
index 000000000..1788e3a44
--- /dev/null
+++ b/docs/examples/catalog_references.py
@@ -0,0 +1,22 @@
+"""Name-based catalog handles: create a catalog and schema, write a table, read it back."""
+
+# --8<-- [start:example]
+import flowfile as ff
+
+SALES = "https://raw.githubusercontent.com/edwardvaneechoud/flowfile/main/data/templates/supermarket_sales.csv"
+
+catalog = ff.CatalogReference("docs_sales", auto_create=True)
+schema = catalog.schema("raw", auto_create=True)
+
+orders = ff.read_csv(SALES).select("invoice_id", "city", "quantity", "gross_income")
+schema.write_table(orders, "orders")
+
+# Use the schema handle anywhere a namespace_id used to be required
+bulk = schema.read_table("orders").filter(ff.col("quantity") > 7)
+schema.write_table(bulk, "bulk_orders")
+# --8<-- [end:example]
+
+result = schema.read_table("bulk_orders").collect()
+assert result.height == 326 # raw rows with quantity > 7 (incl. the 30 seeded duplicates)
+assert set(result.columns) == {"invoice_id", "city", "quantity", "gross_income"}
+assert result["quantity"].min() == 8
diff --git a/docs/examples/code_to_flow.py b/docs/examples/code_to_flow.py
new file mode 100644
index 000000000..dc6ec9bc5
--- /dev/null
+++ b/docs/examples/code_to_flow.py
@@ -0,0 +1,59 @@
+"""The two pipelines from the Building Flows with Code tutorial."""
+
+# --8<-- [start:first]
+import flowfile as ff
+from flowfile import col
+
+df = ff.from_dict({
+ "id": [1, 2, 2],
+ "value": [10, 20, 15]
+})
+
+result = (
+ df.filter(col("value") > 12, description="filter value > 12")
+ .with_columns((col("value") * 10).alias("scaled_value"), description="get a scaled value")
+ .group_by(col("id"))
+ .agg(
+ col("value").sum().alias("sum_value"),
+ col("value").max().alias("max_value"),
+ col("value").min().alias("min_value"),
+ )
+)
+
+frame = result.collect() # a Polars DataFrame
+# --8<-- [end:first]
+
+assert frame.height == 1
+row = frame.row(0, named=True)
+assert row == {"id": 2, "sum_value": 35, "max_value": 20, "min_value": 15}
+
+# --8<-- [start:involved]
+df = ff.from_dict({
+ "id": [1, 2, 3, 4, 5],
+ "category": ["A", "B", "A", "C", "B"],
+ "value": [100, 200, 150, 300, 250]
+})
+
+aggregated = (
+ df.filter(ff.col("value") > 120, description="Filter on value greater than 120")
+ .with_columns(
+ [
+ (ff.col("value") * 1.1).alias("adjusted_value"),
+ ff.when(ff.col("category") == "A").then(ff.lit("Premium"))
+ .when(ff.col("category") == "B").then(ff.lit("Standard"))
+ .otherwise(ff.lit("Basic")).alias("tier"),
+ ],
+ description="Calculate their tier",
+ )
+ .group_by("tier")
+ .agg(
+ ff.col("adjusted_value").sum().alias("total_value"),
+ ff.col("id").count().alias("count"),
+ )
+)
+# --8<-- [end:involved]
+
+tiers = {r["tier"]: r for r in aggregated.collect().to_dicts()}
+assert round(tiers["Standard"]["total_value"], 2) == 495.0 and tiers["Standard"]["count"] == 2
+assert round(tiers["Premium"]["total_value"], 2) == 165.0 and tiers["Premium"]["count"] == 1
+assert round(tiers["Basic"]["total_value"], 2) == 330.0 and tiers["Basic"]["count"] == 1
diff --git a/docs/examples/custom_node.py b/docs/examples/custom_node.py
new file mode 100644
index 000000000..ecd909cd8
--- /dev/null
+++ b/docs/examples/custom_node.py
@@ -0,0 +1,56 @@
+"""Define a custom node with the Node Designer API and run its process() method."""
+
+# --8<-- [start:example]
+import polars as pl
+
+from flowfile import node_designer as nd
+
+
+class GreetingSettings(nd.NodeSettings):
+ main_config: nd.Section = nd.Section(
+ title="Greeting Configuration",
+ description="Configure how to greet each row",
+ name_column=nd.ColumnSelector(
+ label="Name Column",
+ data_types=nd.Types.String,
+ required=True,
+ ),
+ greeting=nd.SingleSelect(
+ label="Greeting",
+ options=[
+ ("formal", "Hello"),
+ ("casual", "Hey"),
+ ],
+ default="casual",
+ ),
+ )
+
+
+class GreetingNode(nd.CustomNodeBase):
+ node_name: str = "Greeting Generator"
+ node_category: str = "Text Processing"
+ title: str = "Add greetings"
+ intro: str = "Prefix a name column with a greeting."
+
+ settings_schema: GreetingSettings = GreetingSettings()
+
+ def process(self, *inputs: pl.DataFrame) -> pl.DataFrame:
+ df = inputs[0]
+ name_col = self.settings_schema.main_config.name_column.value
+ style = self.settings_schema.main_config.greeting.value
+ word = "Hello" if style == "formal" else "Hey"
+ return df.with_columns(
+ pl.concat_str([pl.lit(f"{word}, "), pl.col(name_col)]).alias("greeting")
+ )
+# --8<-- [end:example]
+
+node = GreetingNode()
+node.settings_schema.populate_values(
+ {"main_config": {"name_column": "name", "greeting": "formal"}}
+)
+
+frame = pl.DataFrame({"name": ["Alice", "Bob"]})
+result = node.process(frame)
+
+assert result.columns == ["name", "greeting"]
+assert result["greeting"].to_list() == ["Hello, Alice", "Hello, Bob"]
diff --git a/docs/examples/first_pipeline.py b/docs/examples/first_pipeline.py
new file mode 100644
index 000000000..9d1f79fc9
--- /dev/null
+++ b/docs/examples/first_pipeline.py
@@ -0,0 +1,23 @@
+"""Your first Flowfile pipeline: a derived column, a filter, and a group-by."""
+
+# --8<-- [start:example]
+import flowfile as ff
+
+SALES = "https://raw.githubusercontent.com/edwardvaneechoud/flowfile/main/data/templates/supermarket_sales.csv"
+
+revenue_by_line = (
+ ff.read_csv(SALES)
+ .with_columns((ff.col("unit_price") * ff.col("quantity")).alias("revenue"))
+ .filter(ff.col("quantity") > 5)
+ .group_by("product_line")
+ .agg(ff.col("revenue").sum().alias("total_revenue"))
+ .collect()
+)
+# --8<-- [end:example]
+
+assert revenue_by_line.columns == ["product_line", "total_revenue"]
+assert revenue_by_line.height == 6
+
+by_line = {row["product_line"]: row["total_revenue"] for row in revenue_by_line.to_dicts()}
+assert round(by_line["Fashion accessories"], 2) == 43859.10
+assert round(by_line["Health and beauty"], 2) == 34165.95
diff --git a/docs/examples/integrations/cloud_storage_s3.py b/docs/examples/integrations/cloud_storage_s3.py
new file mode 100644
index 000000000..d06076dbd
--- /dev/null
+++ b/docs/examples/integrations/cloud_storage_s3.py
@@ -0,0 +1,55 @@
+"""Round-trip a Parquet aggregate through S3 with a stored cloud connection."""
+
+import os
+from uuid import uuid4
+
+from pydantic import SecretStr
+
+import flowfile as ff
+from flowfile import FullCloudStorageConnection
+
+# MinIO test fixture wiring — a real S3 bucket needs neither endpoint_url nor these keys.
+endpoint_url = os.environ.get("DOCS_S3_ENDPOINT_URL", "http://localhost:9000")
+ff.create_cloud_storage_connection_if_not_exists(
+ FullCloudStorageConnection(
+ connection_name="analytics-s3",
+ storage_type="s3",
+ auth_method="access_key",
+ aws_region="us-east-1",
+ endpoint_url=endpoint_url,
+ aws_allow_unsafe_html=True,
+ aws_access_key_id="minioadmin",
+ aws_secret_access_key=SecretStr("minioadmin"),
+ )
+)
+s3_path = f"s3://flowfile-test/city_income_{uuid4().hex}.parquet"
+
+# --8<-- [start:example]
+income_by_city = (
+ ff.read_csv("data/templates/supermarket_sales.csv")
+ .group_by("city")
+ .agg(ff.col("gross_income").sum().alias("total_income"))
+)
+
+income_by_city.write_parquet_to_cloud_storage(s3_path, connection_name="analytics-s3")
+
+reloaded = ff.scan_parquet_from_cloud_storage(s3_path, connection_name="analytics-s3").collect()
+# --8<-- [end:example]
+
+from polars.testing import assert_frame_equal
+
+assert reloaded.height == 5
+assert_frame_equal(
+ income_by_city.collect().sort("city"),
+ reloaded.sort("city"),
+ check_row_order=False,
+)
+
+by_city = {row["city"]: round(row["total_income"], 2) for row in reloaded.to_dicts()}
+assert by_city == {
+ "Bago": 3051.94,
+ "Mandalay": 3520.57,
+ "Naypyitaw": 2521.64,
+ "Taunggyi": 3525.60,
+ "Yangon": 3312.56,
+}
diff --git a/docs/examples/integrations/database_read.py b/docs/examples/integrations/database_read.py
new file mode 100644
index 000000000..2c4c93f44
--- /dev/null
+++ b/docs/examples/integrations/database_read.py
@@ -0,0 +1,41 @@
+"""Read from PostgreSQL through a stored database connection."""
+
+import os
+
+import flowfile as ff
+
+# Point at the seeded Postgres test fixture (movies sample database).
+ff.create_database_connection_if_not_exists(
+ "analytics-postgres",
+ database_type="postgresql",
+ host=os.environ.get("DOCS_PG_HOST", "localhost"),
+ port=int(os.environ.get("DOCS_PG_PORT", 5433)),
+ database="testdb",
+ username="testuser",
+ password="testpass",
+)
+
+# --8<-- [start:example]
+big_budget = ff.read_database(
+ "analytics-postgres",
+ query="""
+ SELECT title, budget, vote_average
+ FROM public.movies
+ WHERE budget > 200000000
+ ORDER BY budget DESC
+ """,
+).collect()
+
+all_movies = ff.read_database(
+ "analytics-postgres", schema_name="public", table_name="movies"
+).collect()
+# --8<-- [end:example]
+
+assert all_movies.height == 4803
+assert "title" in all_movies.columns
+
+assert big_budget.height > 0
+assert big_budget["budget"].max() == 380000000
+titles = set(big_budget["title"].to_list())
+assert "Avatar" in titles
+assert "Pirates of the Caribbean: On Stranger Tides" in titles
diff --git a/docs/examples/integrations/kafka_read.py b/docs/examples/integrations/kafka_read.py
new file mode 100644
index 000000000..ff58a702e
--- /dev/null
+++ b/docs/examples/integrations/kafka_read.py
@@ -0,0 +1,63 @@
+"""Read JSON messages from a Kafka topic through a stored Kafka connection."""
+
+import uuid
+
+from flowfile_core.database.connection import get_db_context
+from flowfile_core.database.models import KafkaConnection
+from flowfile_core.kafka.connection_manager import store_kafka_connection
+from flowfile_core.schemas.kafka_schemas import KafkaConnectionCreate
+from test_utils.kafka.fixtures import BOOTSTRAP_SERVERS, create_topic, produce_json_messages
+
+# A unique topic per run so the consumer reads exactly the messages produced here.
+topic = f"docs_orders_{uuid.uuid4().hex[:8]}"
+create_topic(topic, num_partitions=1)
+produce_json_messages(
+ topic,
+ [
+ {"order_id": 1, "product": "keyboard", "amount": 49.99},
+ {"order_id": 2, "product": "mouse", "amount": 19.99},
+ {"order_id": 3, "product": "monitor", "amount": 199.00},
+ ],
+)
+
+# Kafka connections are stored server-side (there is no ff.* helper). Register one
+# pointing at the broker; ff.read_kafka resolves it by name at read time.
+connection_name = f"docs-kafka-{uuid.uuid4().hex[:8]}"
+with get_db_context() as db:
+ store_kafka_connection(
+ db,
+ KafkaConnectionCreate(
+ connection_name=connection_name,
+ bootstrap_servers=BOOTSTRAP_SERVERS,
+ security_protocol="PLAINTEXT",
+ ),
+ user_id=1,
+ )
+
+# --8<-- [start:example]
+import flowfile as ff
+
+orders = ff.read_kafka(
+ connection_name,
+ topic_name=topic,
+ start_offset="earliest",
+ poll_timeout_seconds=10.0,
+).collect()
+# --8<-- [end:example]
+
+assert orders.height == 3
+assert "product" in orders.columns
+assert set(orders["product"].to_list()) == {"keyboard", "mouse", "monitor"}
+# Kafka metadata columns are added alongside the message fields.
+assert "_kafka_offset" in orders.columns
+assert "_kafka_partition" in orders.columns
+
+with get_db_context() as db:
+ conn = (
+ db.query(KafkaConnection)
+ .filter(KafkaConnection.connection_name == connection_name, KafkaConnection.user_id == 1)
+ .first()
+ )
+ if conn is not None:
+ db.delete(conn)
+ db.commit()
diff --git a/docs/examples/ml_pipeline.py b/docs/examples/ml_pipeline.py
new file mode 100644
index 000000000..6fc40174e
--- /dev/null
+++ b/docs/examples/ml_pipeline.py
@@ -0,0 +1,46 @@
+"""Split, train, apply, and evaluate a churn classifier with the FlowFrame ML API."""
+
+# --8<-- [start:example]
+import flowfile as ff
+
+raw = ff.read_csv("data/templates/customer_churn.csv")
+
+# Hold out 20% of rows for evaluation.
+train, test = raw.random_split({"train": 80, "test": 20}, seed=42)
+
+# Fit a binary logistic-regression model on the training slice.
+trained = train.train_model(
+ target="churned",
+ features=["tenure_months", "monthly_charges", "support_calls", "has_contract"],
+ model_type="logistic_regression",
+ params={"l2_reg": 0.1},
+)
+
+# Score the held-out rows. wait_for blocks the apply until the model exists.
+scored = test.wait_for(trained).apply_model(
+ upstream=trained,
+ output_column="predicted_churn",
+)
+
+# Compare predictions against the true labels — a long (metric, value) table.
+metrics = scored.evaluate_model(
+ "churned",
+ predicted_column="predicted_churn",
+ task_type="classification",
+ upstream=trained,
+)
+
+result = metrics.collect()
+# --8<-- [end:example]
+
+assert result.columns == ["metric", "value"]
+assert result.schema["metric"] == ff.String
+assert result.schema["value"] == ff.Float64
+
+reported = set(result["metric"].to_list())
+for metric in ("accuracy", "precision", "recall", "f1", "n_correct", "n_total"):
+ assert metric in reported, f"Expected metric '{metric}' in {reported}"
+
+scored_df = scored.collect()
+assert "predicted_churn" in scored_df.columns
+assert scored_df.height > 0
diff --git a/docs/examples/python_api_aggregations.py b/docs/examples/python_api_aggregations.py
new file mode 100644
index 000000000..65d52a197
--- /dev/null
+++ b/docs/examples/python_api_aggregations.py
@@ -0,0 +1,49 @@
+"""Grouped aggregations, the all_ selector, and window functions (rank and cum_sum over groups)."""
+
+# --8<-- [start:example]
+import flowfile as ff
+
+# One group_by node fuses the grouping and every aggregation.
+summary = (
+ ff.read_csv("data/templates/orders.csv")
+ .group_by("customer_id")
+ .agg(
+ ff.col("order_id").count().alias("n_orders"),
+ ff.col("quantity").sum().alias("total_qty"),
+ ff.col("quantity").mean().alias("avg_qty"),
+ ff.col("quantity").min().alias("min_qty"),
+ ff.col("quantity").max().alias("max_qty"),
+ ff.col("quantity").std().alias("std_qty"),
+ ff.col("quantity").first().alias("first_qty"),
+ ff.col("quantity").implode().alias("all_qty"),
+ )
+ .collect()
+)
+
+# Sum every numeric column at once with the all_ selector inside select.
+totals = (
+ ff.read_csv("data/templates/orders.csv")
+ .select(ff.col("quantity"))
+ .select(ff.all_().sum())
+ .collect()
+)
+
+# Window functions: rank and running total within each customer.
+ranked = (
+ ff.read_csv("data/templates/orders.csv")
+ .with_columns(
+ ff.col("quantity").rank(method="dense", descending=True).over("customer_id").alias("qty_rank"),
+ ff.col("quantity").cum_sum().over("customer_id").alias("cum_qty"),
+ )
+ .collect()
+)
+# --8<-- [end:example]
+
+assert summary.height == 401
+assert summary["n_orders"].sum() == 1000
+assert summary["total_qty"].sum() == 5461
+
+assert totals.to_dicts()[0]["quantity"] == 5461
+
+assert ranked.height == 1000
+assert ranked["qty_rank"].min() == 1
diff --git a/docs/examples/python_api_joins.py b/docs/examples/python_api_joins.py
new file mode 100644
index 000000000..05f5deb67
--- /dev/null
+++ b/docs/examples/python_api_joins.py
@@ -0,0 +1,51 @@
+"""Join types (inner/left/full/semi/anti/cross), concatenation with ff.concat, and join validation."""
+
+# --8<-- [start:example]
+import flowfile as ff
+
+orders = ff.read_csv("data/templates/orders.csv")
+customers = ff.read_csv("data/templates/customers.csv")
+products = ff.read_csv("data/templates/products.csv")
+
+# Enrich orders with customer and product attributes.
+inner = orders.join(customers, on="customer_id", how="inner")
+with_product = orders.join(products, on="product_id", how="left")
+
+# full keeps unmatched rows from both sides; semi/anti filter the left side.
+full = orders.join(customers, on="customer_id", how="full")
+repeat_customers = customers.join(orders, on="customer_id", how="semi")
+dormant_customers = customers.join(orders, on="customer_id", how="anti")
+
+# Stack frames vertically with ff.concat (there is no vstack), then dedupe.
+deduped = ff.concat([customers, customers]).unique()
+
+# Validate a join key before joining: compare distinct keys to total rows.
+key_stats = customers.select(
+ ff.col("customer_id").n_unique().alias("distinct_customers"),
+ ff.col("customer_id").count().alias("total_rows"),
+)
+
+inner_df = inner.collect()
+full_df = full.collect()
+repeat_df = repeat_customers.collect()
+dormant_df = dormant_customers.collect()
+key_stats_df = key_stats.collect()
+# --8<-- [end:example]
+
+with_product_df = with_product.collect()
+deduped_df = deduped.collect()
+
+assert inner_df.height == 1000
+assert "name" in inner_df.columns
+assert with_product_df.height == 1000
+
+assert full_df.height == 1099
+assert repeat_df.height == 401
+assert dormant_df.height == 99
+assert repeat_df.height + dormant_df.height == 500
+
+assert deduped_df.height == 500
+
+stats = key_stats_df.to_dicts()[0]
+assert stats["distinct_customers"] == 500
+assert stats["total_rows"] == 500
diff --git a/docs/examples/python_api_operations.py b/docs/examples/python_api_operations.py
new file mode 100644
index 000000000..2737dfd0b
--- /dev/null
+++ b/docs/examples/python_api_operations.py
@@ -0,0 +1,52 @@
+"""Row and column operations: unique, cast, filter, sort, exclude, and string/date/window expressions."""
+
+# --8<-- [start:example]
+import flowfile as ff
+
+# Deduplicate, derive columns, filter, sort, then drop a column with a selector.
+cleaned = (
+ ff.read_csv("data/templates/orders.csv")
+ .unique(subset=["order_id"])
+ .with_columns(
+ (ff.col("quantity") * ff.lit(2)).alias("double_qty"),
+ ff.col("quantity").cast(ff.Float64).alias("quantity_f"),
+ )
+ .filter(ff.col("quantity") >= 3)
+ .sort("quantity", descending=True)
+ .select(ff.col("*").exclude("product_id"))
+ .collect()
+)
+
+# String and date namespaces on expressions.
+enriched = (
+ ff.read_csv("data/templates/customers.csv")
+ .with_columns(
+ ff.col("name").str.to_uppercase().alias("name_upper"),
+ ff.col("email").str.slice(0, 5).alias("email_prefix"),
+ ff.col("name").str.contains("a").alias("name_has_a"),
+ ff.col("signup_date").dt.year().alias("signup_year"),
+ ff.col("signup_date").dt.weekday().alias("signup_weekday"),
+ )
+ .collect()
+)
+
+# A running total per customer with a window (cum_sum over a group).
+running = (
+ ff.read_csv("data/templates/orders.csv")
+ .sort("order_date")
+ .with_columns(ff.col("quantity").cum_sum().over("customer_id").alias("running_qty"))
+ .collect()
+)
+# --8<-- [end:example]
+
+assert "product_id" not in cleaned.columns
+assert "double_qty" in cleaned.columns and "quantity_f" in cleaned.columns
+assert cleaned.height == 800
+
+assert enriched.height == 500
+assert enriched["signup_weekday"].min() >= 1
+assert enriched["signup_weekday"].max() <= 7
+assert enriched["email_prefix"].str.len_chars().max() <= 5
+
+assert running.height == 1000
+assert running["running_qty"].max() > 0
diff --git a/docs/examples/sales_pipeline.py b/docs/examples/sales_pipeline.py
new file mode 100644
index 000000000..d8e8af031
--- /dev/null
+++ b/docs/examples/sales_pipeline.py
@@ -0,0 +1,33 @@
+"""Dedupe, filter, and aggregate supermarket sales by city with the FlowFrame API."""
+
+# --8<-- [start:example]
+import flowfile as ff
+
+SALES = "https://raw.githubusercontent.com/edwardvaneechoud/flowfile/main/data/templates/supermarket_sales.csv"
+
+result = (
+ ff.read_csv(SALES)
+ .unique()
+ .filter(ff.col("quantity") > 7)
+ .group_by("city")
+ .agg(
+ ff.col("gross_income").sum().alias("total_income"),
+ ff.col("gross_income").median().alias("median_income"),
+ )
+ .collect()
+)
+# --8<-- [end:example]
+
+by_city = {row["city"]: row for row in result.to_dicts()}
+
+assert result.height == 5
+assert set(by_city) == {"Bago", "Mandalay", "Naypyitaw", "Taunggyi", "Yangon"}
+
+assert round(by_city["Bago"]["total_income"], 2) == 1429.66
+assert round(by_city["Mandalay"]["total_income"], 2) == 1846.88
+assert round(by_city["Naypyitaw"]["total_income"], 2) == 1186.66
+assert round(by_city["Taunggyi"]["total_income"], 2) == 1635.53
+assert round(by_city["Yangon"]["total_income"], 2) == 1704.81
+
+assert round(by_city["Mandalay"]["median_income"], 3) == 27.220
+assert round(by_city["Yangon"]["median_income"], 3) == 26.085
diff --git a/docs/for-developers/ai-architecture.md b/docs/for-developers/ai-architecture.md
index 692b33df2..fc5d3f03f 100644
--- a/docs/for-developers/ai-architecture.md
+++ b/docs/for-developers/ai-architecture.md
@@ -159,6 +159,9 @@ So every AI endpoint inherits the gate without per-route boilerplate. The Mutabl
## Routes catalog
+!!! info "Verify against the package"
+ The routes and Pydantic model names below are a snapshot of the `ai/*_routes.py` files. Request/response model names in particular drift as the surface evolves — treat this table as a map, and read the matching `*_routes.py` for the authoritative signature before depending on one.
+
| Route | Method | Request | Response | Source |
|-------|--------|---------|----------|--------|
| `/ai/health` | GET | — | `{"status": "skeleton"}` | `routes.py` |
@@ -230,10 +233,13 @@ Per-vendor subclasses override class-level fields:
| `AnthropicProvider` | `anthropic` | `anthropic/` | `claude-sonnet-4-6` | Haiku 4.5 for Cmd+K / ghost / autocomplete / agent_staged; Opus 4.7 for `agent_complex` |
| `OpenAIProvider` | `openai` | `""` | `gpt-4.1-mini` | `gpt-4.1` for `explain` / `agent_complex` / `docgen` / `lineage` |
| `GoogleProvider` | `google` | `gemini/` | `gemini-2.5-flash` | `gemini-2.5-pro` for `agent_complex` |
-| `GroqProvider` | `groq` | `groq/` | `qwen/qwen3-coder-30b-a3b-instruct` | One model across surfaces |
+| `GroqProvider` | `groq` | `groq/` | `qwen/qwen3-32b` | `qwen/qwen3-coder-30b-a3b-instruct` for most surfaces (ghost / explain / agent_* / docgen / lineage / …); `qwen/qwen3-32b` for `cmd_k` |
| `OpenRouterProvider` | `openrouter` | `openrouter/` | `qwen/qwen3-coder-30b-a3b-instruct` | `meta-llama/llama-3.3-70b-instruct` for `agent_staged` (free tier) |
| `OllamaProvider` | `ollama` | `ollama_chat/` | `llama3.1:8b` | `llama3.1:70b` for `agent_complex`. `default_api_base="http://localhost:11434"` |
+!!! note "`default_model` vs surface models"
+ `default_model` is the terminal fallback the class uses when no other rule fires (see the resolution order below). It is **not** the model most surfaces run on — `surface_models[surface]` usually shadows it. Groq is a good example: the class default is `qwen/qwen3-32b`, but every surface except `cmd_k` maps to `qwen/qwen3-coder-30b-a3b-instruct`. The model names in this table rot as vendors rename releases; check each provider's `surface_models` dict in `providers/{name}.py` for the current values.
+
`provider_factory(name, *, model, surface, api_key, api_base)` in `providers/registry.py` is the construction entry point. The `PROVIDERS` dict registers all six classes; `list_supported_providers()` returns the names in registration order.
---
@@ -537,7 +543,7 @@ Components in `features/ai/`:
- `AiAssistant.vue` — the drawer (chat + agent).
- `AiAgentRun.vue` / `AiAgentEvent.vue` — agent panel and per-event renderers.
- `AiDiffPanel.vue` / `AiDiffPreview.vue` — diff review UI.
-- `AiInlineActions.vue` — the ✨ menu on a node.
+- `AiInlineActions.vue` — the inline-actions (sparkle) menu on a node.
- `AiCommandPalette.vue` — Cmd+K.
- `AiGhostNode.vue` — edge-stub suggestions.
- `AiMentionAutocomplete.vue` (+ `mentionVocabulary.ts`) — `@flow` / `@node:id` completion.
diff --git a/docs/for-developers/architecture.md b/docs/for-developers/architecture.md
index 51556a41c..1450eee30 100644
--- a/docs/for-developers/architecture.md
+++ b/docs/for-developers/architecture.md
@@ -1,6 +1,6 @@
# Technical Architecture
-Flowfile's architecture integrates visual design with high-performance data processing through three interconnected services and utilizes Polars' lazy evaluation. This design provides real-time feedback, processes large datasets efficiently, and maintains UI responsiveness during intensive computations. On this page, the three-service architecture, key technical features such as real-time schema prediction and efficient data exchange, and the role of Polars' lazy evaluation are explained.
+Flowfile's architecture pairs visual design with data processing across three services, built on Polars' lazy evaluation. This page explains the three-service architecture, key technical features such as real-time schema prediction and efficient data exchange, and the role of Polars' lazy evaluation.

@@ -8,7 +8,7 @@ Flowfile's architecture integrates visual design with high-performance data proc
### Three-Service Architecture
-**Designer (Electron + Vue)** The visual interface where data pipelines are build through drag-and-drop operations. It communicates with the Core service to provide real-time feedback and displays data previews.
+**Designer (Tauri + Vue)** The visual interface where data pipelines are built through drag-and-drop operations. It is a Tauri 2 desktop shell (Rust) wrapping a Vue 3 renderer, and the same renderer serves the web build. It communicates with the Core service for real-time feedback and data previews.
**Core Service (FastAPI)** The orchestration engine that manages workflows, predicts schemas, and coordinates execution. It maintains the Directed Acyclic Graph (DAG) structure and handles all UI interactions and overall flow logic.
@@ -18,7 +18,7 @@ Flowfile's architecture integrates visual design with high-performance data proc
### Real-time Schema Prediction
-When you add or configure a node, Flowfile immediately shows how your data structure will change — **without executing any transformations**. This continuous feedback significantly reduces user errors and increases predictability. This happens through:
+When you add or configure a node, Flowfile immediately shows how your data structure will change — **without executing any transformations**. This happens through:
- **Schema Callbacks**: Custom functions, defined per node type, that calculate output schemas based on node settings and input schemas.
- **Lazy Evaluation**: Leveraging Polars' ability to determine the schema of a planned transformation (`LazyFrame`) without processing the full dataset.
@@ -48,51 +48,41 @@ As you add and connect nodes, Flowfile builds a Directed Acyclic Graph (DAG) whe
* **Nodes** represent data operations (read file, filter, join, write to database, etc.).
* **Edges** represent the flow of data between operations.
-The DAG is managed by the `EtlGraph` class in the Core service, which orchestrates the entire workflow:
+The DAG is managed by the `FlowGraph` class (`flowfile_core/flowfile_core/flowfile/flow_graph.py`) in the Core service, which orchestrates the entire workflow. The class shape below is illustrative — see the [Python API Reference](python-api-reference.md#flowgraph) for the real signatures.
-View EtlGraph Python Implementation
+View FlowGraph shape (illustrative)
```python
-class EtlGraph:
+class FlowGraph:
"""
Manages the ETL workflow as a DAG. Stores nodes, dependencies,
and settings, and handles the execution order.
"""
uuid: str
- _node_db: Dict[Union[str, int], NodeStep] # Internal storage for all node steps
- _flow_starts: List[NodeStep] # Nodes that initiate data flow (e.g., readers)
+ _node_db: Dict[Union[str, int], FlowNode] # Internal storage for all nodes
+ _flow_starts: List[FlowNode] # Nodes that initiate data flow (e.g., readers)
_node_ids: List[Union[str, int]] # Tracking node identifiers
flow_settings: schemas.FlowSettings # Global configuration for the flow
def add_node_step(self, node_id: Union[int, str], function: Callable,
node_type: str, **kwargs) -> None:
- """Adds a new processing node (NodeStep) to the graph."""
- node_step = NodeStep(node_id=node_id, function=function, node_type=node_type, **kwargs)
- self._node_db[node_id] = node_step
- # Additional logic to manage dependencies and flow starts...
+ """Adds a new FlowNode to the graph."""
+ ...
def run_graph(self) -> RunInformation:
- """Executes the entire flow in the correct topological order."""
- execution_order = self.topological_sort() # Determine correct sequence
- run_info = RunInformation()
- for node in execution_order:
- # Execute node based on mode (Development/Performance)
- node_results = node.execute_node() # Simplified representation
- run_info.add_result(node.node_id, node_results)
- return run_info
-
- def topological_sort(self) -> List[NodeStep]:
- """Determines the correct order to execute nodes based on dependencies."""
- # Standard DAG topological sort algorithm...
- pass
+ """Executes the flow in the correct topological order."""
+ ...
```
-Each `NodeStep` in the graph encapsulates information about its dependencies, transformation logic, and output schema. This structure allows Flowfile to determine execution order, track data lineage, optimize performance, and provide schema predictions throughout the pipeline.
+Each `FlowNode` in the graph encapsulates its dependencies, transformation logic, and output schema. This lets Flowfile determine execution order, track data lineage, optimize performance, and predict schemas throughout the pipeline.
### Execution Modes
+!!! info "Canonical explanation"
+ This is the reference description of Development vs Performance mode. The [Core Developer Guide](flowfile-core.md#execution-strategy-how-nodes-decide-where-to-run) and [Design Philosophy](design-philosophy.md) pages link here rather than repeat it.
+
By clicking on settings → execution modes you can set how the flow will be executed the next time you run the flow.

@@ -115,25 +105,28 @@ In Development mode, each node's transformation is triggered sequentially within
In Performance mode, Flowfile fully embraces Polars' lazy evaluation. The Core service constructs the *entire* Polars execution plan based on the DAG. This plan (`LazyFrame`) is passed to the Worker service. The Worker only *materializes* (executes `.collect()` or `.sink_*()`) the plan when an output node (like writing to a file) requires the final result, or if a node is explicitly configured to cache its results (`node.cache_results`). This minimizes computation and memory usage by avoiding unnecessary intermediate materializations.
-View Performance Mode Python Example
+View Performance Mode Python Example (simplified)
```python
# Execution logic in Performance Mode (simplified)
-def execute_performance_mode(self, node: NodeStep, is_output_node: bool):
+def execute_performance_mode(self, node: FlowNode, is_output_node: bool):
"""Handles execution in performance mode, leveraging lazy evaluation."""
if is_output_node or node.cache_results:
- # If result is needed (output or caching), trigger execution in Worker
- external_df_fetcher = ExternalDfFetcher(
- lf=node.get_resulting_data().data_frame, # Pass the LazyFrame plan
- file_ref=node.hash, # Unique reference for caching
- wait_on_completion=False # Usually async
+ # If the result is needed (output or caching), offload to the Worker.
+ # Offload happens inside ExternalDfFetcher.__init__ — constructing it
+ # serializes the LazyFrame and POSTs it to the worker. flow_id and
+ # node_id are required.
+ fetcher = ExternalDfFetcher(
+ flow_id=node.flow_id,
+ node_id=node.node_id,
+ lf=node.get_resulting_data().data_frame, # the LazyFrame plan
+ file_ref=node.hash, # unique reference for caching
+ wait_on_completion=False, # usually async
)
- # Worker executes .collect() or .sink_*() and caches if needed
- result = external_df_fetcher.get_result() # May return LazyFrame or trigger compute
- return result # Or potentially just confirmation if sinking
+ result = fetcher.get_result() # Worker runs .collect()/.sink_*() and caches
+ return result
else:
- # If not output/cached, just pass the LazyFrame plan along
- # No computation happens here for intermediate nodes
+ # Intermediate nodes just pass the LazyFrame plan along — no compute here.
return node.get_resulting_data().data_frame
```
@@ -152,36 +145,36 @@ Flowfile uses Apache Arrow IPC format for efficient inter-process communication
This ensures responsiveness, memory isolation, and efficiency.
-Here’s how the Core might offload computation to the Worker, and how the Worker manages the separate process execution:
+Here is how the Core offloads computation to the Worker, and how the Worker manages the separate process execution:
-View Core-Side Python Example
+View Core-Side Python Example (simplified)
```python
# Core side - Initiating remote execution in the Worker (simplified)
def execute_remote(self, performance_mode: bool = False) -> None:
"""Offloads the execution of a node's LazyFrame to the Worker service."""
- # Create a fetcher instance to manage communication with the Worker
- external_df_fetcher = ExternalDfFetcher(
- lf=self.get_resulting_data().data_frame, # The Polars LazyFrame plan
- file_ref=self.hash, # Unique identifier for the result/cache
- wait_on_completion=False, # Operate asynchronously
+ # Constructing ExternalDfFetcher IS the offload: __init__ serializes the
+ # LazyFrame and sends it to the worker (no separate "start" call). The
+ # constructor requires flow_id and node_id.
+ fetcher = ExternalDfFetcher(
+ flow_id=self.flow_id,
+ node_id=self.node_id,
+ lf=self.get_resulting_data().data_frame, # the Polars LazyFrame plan
+ file_ref=self.hash, # unique identifier for result/cache
+ wait_on_completion=False, # operate asynchronously
)
- # Store the fetcher to potentially retrieve results later
- self._fetch_cached_df = external_df_fetcher
-
- # Request the Worker to start processing (this returns quickly)
- # The actual computation happens asynchronously in the Worker
- external_df_fetcher.start_processing_in_worker() # Hypothetical method name
+ # Store the fetcher to retrieve results later
+ self._fetch_cached_df = fetcher
# For UI updates, request a sample separately
- self.store_example_data_generator(external_df_fetcher) # Fetches sample async
+ self.store_example_data_generator(fetcher) # fetches sample async
```
-View Worker-Side Python Example
+View Worker-Side Python Example (simplified)
```python
# Worker side - Managing computation in a separate process (simplified)
@@ -192,8 +185,10 @@ def start_process(
# ... other args like operation type
) -> None:
"""Launches a separate OS process to handle the heavy computation."""
- # Use multiprocessing context for safety
- mp_context = multiprocessing.get_context('spawn') # or 'fork' depending on OS/needs
+ # The worker forces spawn at module load (set_start_method('spawn',
+ # force=True) in flowfile_worker/__init__.py) so every child is a fresh,
+ # killable process regardless of platform — never fork.
+ mp_context = get_context('spawn')
# Shared memory/queue for progress tracking and results/errors
progress = mp_context.Value('i', 0) # Shared integer for progress %
@@ -219,7 +214,7 @@ def start_process(
```
-## The Power of Lazy Evaluation
+## Lazy Evaluation
By building on Polars' lazy evaluation, Flowfile achieves:
@@ -228,15 +223,6 @@ By building on Polars' lazy evaluation, Flowfile achieves:
- **Parallel Execution**: Polars automatically parallelizes operations across all available CPU cores during execution.
- **Predicate Pushdown**: Filters and selections are applied as early as possible in the plan, often directly at the data source level (like during file reading), minimizing the amount of data that needs to be processed downstream.
-## Summary
-
-This design enables Flowfile to:
-
-- Provide instant feedback without processing data.
-- Handle datasets of any size efficiently.
-- Keep the UI responsive during heavy computations.
-- Scale from simple transformations to complex ETL pipelines.
-
---
-*For a deep dive into the implementation details, see the [full technical article](https://dev.to/edwardvaneechoud/building-flowfile-architecting-a-visual-etl-tool-with-polars-576c).*
\ No newline at end of file
+*Background reading: the original [design article](https://dev.to/edwardvaneechoud/building-flowfile-architecting-a-visual-etl-tool-with-polars-576c) covers the motivation and early design. It predates some renames (the graph class is now `FlowGraph`, nodes are `FlowNode`), so treat class names there as historical.*
\ No newline at end of file
diff --git a/docs/for-developers/catalog-architecture.md b/docs/for-developers/catalog-architecture.md
new file mode 100644
index 000000000..dedaffe65
--- /dev/null
+++ b/docs/for-developers/catalog-architecture.md
@@ -0,0 +1,113 @@
+# Catalog Architecture
+
+The catalog is Flowfile's largest subsystem: namespaces, physical and virtual tables, SQL, visualizations, dashboards, notebooks, schedules, runs, artifacts, and the sharing model all live behind one facade. This page is the contributor's map — how the pieces compose, where metadata and data actually live, the resolution paths that matter, and the invariants you must not break. It pairs with [Kernel Architecture](kernel-architecture.md) and [Visualizations & Graphic Walker](visualizations.md), which own their subsystems' deep dives.
+
+## The big picture
+
+Architecturally the catalog is **two substrates and a service layer**. Metadata — every namespace, table row, schedule, grant — lives in the shared SQLite catalog DB. Table *data* lives outside the DB as Delta directories (locally or on S3). Around those, per-domain services implement features, and everything heavy routes to neighbors: full-frame reads to the worker, user code to kernels, timed execution to the embedded scheduler.
+
+
+
+## Metadata vs data
+
+**Metadata** is SQLAlchemy models in `database/models.py`: `CatalogNamespace` (two-level catalog→schema tree; level-0 rows may carry `storage_uri`/`storage_connection_name`), `FlowRegistration` (+ stable `flow_uuid`), `FlowRun` (with a YAML `flow_snapshot`), `FlowSchedule` + `ScheduleTriggerTable`, `CatalogTable`, `CatalogTableReadLink` (read-lineage), `CatalogVisualization`, `CatalogDashboard`, `CatalogNotebook` (metadata only — cells live on disk), `GlobalArtifact` (metadata only — blobs live on the kernel shared volume), engagement tables, and the polymorphic `ResourceGrant` layer.
+
+**Data** is one Delta directory per table under `storage.catalog_tables_directory` (`_/` with `_delta_log/`; legacy single `.parquet` files still open — `delta_utils.py` detects the format). A `CatalogTable` row points at its directory via `file_path` and caches `schema_json`, `row_count`, `size_bytes` — for optimized virtual tables `file_path` is NULL because there is no data on disk at all.
+
+**The S3 backend is per-catalog, not global.** A level-0 namespace's `storage_uri` + `storage_connection_name` make *new* tables under that catalog write to object storage; existing local tables never move, and the metadata DB stays local. `storage_backend.resolve_for_namespace` returns a `CatalogStorageTarget` with two credential forms: decrypted `storage_options` for core's own bounded reads, and an **owner-encrypted** `worker_interface` for the worker hand-off — secrets never cross the wire in plaintext, and credentials always resolve as the *catalog owner*, never the calling user. `FLOWFILE_CATALOG_STORAGE_URI`/`_CONNECTION` are snapshotted onto a catalog once at creation, not read live.
+
+## The service layer
+
+`CatalogService` (`catalog/service.py`) is a **facade over composed sub-services**, one per domain under `catalog/services/`: namespaces, flows, runs, engagement, schedules, tables, virtual_tables, sql, previews, visualizations, notebooks, stats. Composition is constructor injection with two deliberate quirks:
+
+- **Cyclic late-binding** — `SqlService` and `VirtualTableService` need each other (SQL resolution resolves virtual tables; creating a query-virtual derives its schema by running SQL). The cycle is broken with `.bind()` calls after construction.
+- **Facade back-binding** — visualization/SQL/schedule services call back through the facade (`bind_facade(self)`) so tests can monkeypatch `CatalogService` methods and have the patch take effect on internal paths.
+
+Data access goes exclusively through `CatalogRepository` (protocol) / `SQLAlchemyCatalogRepository`. Services raise the `CatalogError` hierarchy (`catalog/exceptions.py`), never `HTTPException`; the router (`routes/catalog.py`, mounted at `/catalog`) is a thin adapter that maps exceptions to status codes via `_CATALOG_EXCEPTION_MAP` and builds a fresh repository + service per request. The router-level auth dependency accepts a JWT **or** the kernel's `X-Internal-Token`.
+
+## Access control
+
+Authorization is the facade's job, implemented by `AccessResolver` (`catalog/access.py`) over `auth/sharing.py`:
+
+- The router always injects a resolver, but it only *restricts* when `sharing_enabled() and not user.is_admin and not is_synthetic_principal(user)`. Electron/single-user mode, admins, the kernel's `_internal_service` principal, and internal callers that construct `CatalogService` without an `access` argument (scheduler, flow execution, tests) are unrestricted.
+- Resolution is **own-first, then group-granted**: accessible ids = rows the user owns ∪ ids granted to any of their groups, at `use` or `manage` level. A use-level grant is read-only — `writable_namespace_ids` requires ownership or a *manage* grant.
+- **Namespace grants cascade**: a grant on a catalog or schema reaches the tables, flows, visualizations, dashboards, notebooks, and artifacts inside it (one-level child expansion, memoized per request). Tree listing prunes invisible items but keeps "context-only" ancestor namespaces as breadcrumbs with redacted descriptions, and stamps every DTO with its effective access level.
+
+
+
+!!! warning "The delete invariant"
+ Every resource-delete path must call `sharing.delete_grants_for_resource`. SQLite reuses rowids, so a surviving grant row would silently attach itself to a future, unrelated resource. The repository does this on every delete and an ORM `after_delete` backstop covers `session.delete` — but bulk `query.delete()` bypasses ORM events and must clean up explicitly.
+
+## Virtual tables
+
+A `catalog_writer` node in virtual mode registers a table that stores no data; what gets stored decides how reads resolve later:
+
+- At registration, `FlowGraph.check_flow_laziness` walks the writer's upstream using each node's declared `laziness` (`configs/node_store/nodes.py`). If everything is lazy *and* no source is on cloud storage, the write is **optimized**: the LazyFrame plan is serialized into `CatalogTable.serialized_lazy_frame`, together with the Delta versions of its source tables (`source_table_versions`) and a human-readable `polars_plan`.
+- **Optimized resolution** deserializes that plan — no flow run — after `check_source_versions_current` confirms every recorded source Delta version still matches; staleness falls through to the standard path.
+- **Standard resolution** opens the producer flow, finds the `catalog_writer` node whose configured `table_name` matches the table's name (that name-keying is what lets one producer flow back several virtual tables), and executes just that node in performance mode — on the worker when offload is enabled.
+- **Serialized cloud plans are never replayed**: a serialized cloud scan embeds decrypted `storage_options`, so `serialized_frame_uses_cloud` forces such tables onto the standard path permanently.
+
+**Query virtual tables** (the SQL editor's "save as virtual table") store a `sql_query` instead of a plan. Resolution builds a `pl.SQLContext`, registering every referenced table and recursively resolving nested virtuals — bounded by `QUERY_VIRTUAL_TABLE_RECURSION_LIMIT = 5` plus a visited-set cycle check, with per-table grant checks in restricted mode.
+
+
+
+## SQL execution
+
+`SqlService.execute_sql_query` resolves every accessible table into a name map (qualified `catalog.schema.table`, `schema.table`, and bare name when unique), materializes referenced virtual tables to IPC files via the worker, then hands the whole query to the worker: a spawned child opens the Delta directories and IPC files in a `pl.SQLContext`, collects at most `max_rows`, and returns rows. **Core never collects** — it ships names and paths. Cloud-backed physical tables are excluded from this path with a clear error (the in-flow `sql_query` node has its own resolution path that handles them). "Save as flow" generates a `catalog_reader`-per-table → `sql_query` flow and registers it.
+
+## Schedules and triggers
+
+Schedule rows live in the shared DB, and the embedded scheduler (`flowfile_scheduler/engine.py`) deliberately **imports nothing from core** — it reads mirrored models from `shared/`, polls every 30 s, and coordinates instances through a single-row advisory lock with a 90 s stale-takeover.
+
+Table triggers fire through **two cooperating paths**:
+
+- **Push (synchronous)**: catalog writes call `safely_fire_table_trigger_schedules` — find enabled watchers, skip any with an active run, commit `last_trigger_table_updated_at` *before* spawning (that pre-commit is the double-fire guard), then spawn the run.
+- **Poll (safety net)**: every tick the scheduler compares each watched table's `updated_at` with the schedule's recorded value and fires on newer. Set-triggers require *all* linked tables updated since the last fire.
+
+Fired schedules execute headlessly: `spawn_flow_run` creates the `FlowRun` row and spawns a subprocess (`flowfile run flow --run-id `, or the frozen-binary equivalent), which reports completion back through `shared.run_completion` — again with no core import. Maintenance operations (optimize/vacuum) deliberately keep `updated_at` unchanged so they never masquerade as data changes and fire triggers.
+
+
+
+## Notebooks
+
+Notebooks are the catalog's exploration console, and architecturally they're a deliberate split-identity design: **the DB row is registration, the file is the content.** A `CatalogNotebook` row carries name, namespace placement, owner, and `default_kernel_id`; the cells live on disk as one deterministic YAML file per notebook at `//.notebook.yaml` (`catalog/services/notebook_store.py`). That's the same row-plus-file pattern flows use (`FlowRegistration` + its `.yaml`), and for the same reason: it's what lets [Projects](../users/projects.md) version notebooks in git.
+
+The store is built for that job — atomic writes, fixed key order, multi-line cell `source` dumped as YAML literal blocks so diffs read like code review, and a server-derived filename (nothing user-controlled in the path). Reads degrade gracefully: a missing or torn file yields an empty notebook and a malformed cell is skipped rather than raising, so a bad `git merge` on a notebook file can't brick the catalog.
+
+Three design decisions worth knowing before touching this code:
+
+- **`NotebookService` owns no execution.** The cell model is mixed — `python` / `sql` / `markdown`, with per-cell `metadata` (e.g. SQL `max_rows`) — and each type routes elsewhere: SQL cells to the same `/catalog/sql/execute` worker path as the SQL editor, Python cells to the kernel's `execute_cell` with the full `flowfile_ctx` API ([Kernel Architecture](kernel-architecture.md)), Markdown rendered client-side. The shipped editor currently surfaces Python and Markdown cells (`notebook/types.ts` `CellType`); SQL cells are schema-supported ahead of the UI. The notebook service only persists.
+- **`default_kernel_id` is a plain string, no FK — by design.** It just remembers the last kernel picked for Python cells; deleting that kernel means "nothing pre-selected," never a cascade into notebook rows.
+- **The integer `id` is the public handle**: it's the grant key for [sharing](#access-control) (`catalog_notebook` is namespace-scoped, so catalog grants cascade to notebooks) and the frontend derives the kernel session key from it. The `notebook_uuid` exists for the on-disk filename's stability, not for addressing.
+
+Cell editing gets Jedi-backed code intelligence from core's LSP routes (`flowfile_core/lsp/`) — completions run server-side against the kernel's environment, not in the browser.
+
+
+
+## Neighbors
+
+- **Worker** — the only sanctioned reader of full table data: `catalog_reader.open_catalog_table` (Delta, local or cloud) and `open_virtual_result` (IPC), invoked through core's `trigger_*` calls; results are collected in spawned children and returned as paths/rows, never frames. Long-lived viz session children are the same pattern specialized for Graphic Walker — see [Visualizations](visualizations.md).
+- **Kernels** — write Delta directories *directly* on the shared volume, read the resulting metadata from the Delta log without materializing, and POST it to core (`/catalog/tables/from-data` or `/{id}/refresh`); core stores the pre-computed metadata as-is and never scans the file. Kernel-exchange and artifact directories must stay under the kernel shared volume — see [Kernel Architecture](kernel-architecture.md).
+- **Artifacts** — `GlobalArtifact` rows are metadata plus a `storage_key`; blobs live on the kernel shared volume with versioning and soft delete.
+- **Projects** — catalog mutations fire never-raising `project_sync` hooks that mirror *metadata* (namespaces, table pointers, schedules, notebooks, model metadata — no data, no blobs, secrets as `${secret:NAME}` placeholders) into the active git project. The DB remains the runtime source of truth.
+
+## Invariants
+
+| Rule | Why |
+|---|---|
+| Core never collects full catalog frames — worker children own dataset memory | The core/worker contract; previews are the only bounded exception |
+| Every delete calls `delete_grants_for_resource` (bulk deletes explicitly) | SQLite rowid reuse would re-attach stale grants |
+| Serialized cloud plans are never deserialized | They embed decrypted storage credentials |
+| Storage credentials resolve as the catalog owner; worker payloads stay owner-encrypted | Sharing must not leak or re-key secrets |
+| Catalog storage is immutable once a physical table exists; changing it requires ownership | Prevents splitting one catalog's data across backends |
+| `sharing_enabled()` reads `FLOWFILE_MODE` from `os.environ` per call | Import-time caching would make docker-mode untestable |
+| optimize/vacuum never bump `updated_at` | Maintenance must not fire table triggers |
+| Table names reject dots | Dots are the SQL qualification separator |
+
+## Extension points
+
+**A new catalog resource type** touches, in order: the model in `database/models.py` plus an Alembic migration (next `NNN_` prefix); a `ResourceSpec` in `sharing.RESOURCE_REGISTRY` (and `_NAMESPACE_SCOPED_TYPES` if namespace grants should cascade to it); a sub-service under `catalog/services/` wired into `CatalogService.__init__`; repository methods whose delete path cleans up grants; DTOs in `schemas/catalog_schema.py`; an exception in `catalog/exceptions.py` mapped in `_CATALOG_EXCEPTION_MAP`; router endpoints; access guards and DTO stamping; and a `project_sync` hook if it should be projectable.
+
+**A new schedule kind** adds the `schedule_type` semantics and validators, a `_process_*` method in the scheduler's `_tick`, and — if push-triggered — a fan-out method in `schedules.py`. Keep the scheduler free of core imports; reflect any new columns via `shared/models.py`.
+
+**A new storage backend** extends `storage_backend.py` (URI schemes, `CatalogStorageTarget`, worker payload) and mirrors the join/decrypt on the worker side in `catalog_reader.py`; blob artifacts implement the `shared.artifact_storage.ArtifactStorageBackend` ABC.
diff --git a/docs/for-developers/creating-custom-nodes.md b/docs/for-developers/creating-custom-nodes.md
index a9e8a218d..de58afc17 100644
--- a/docs/for-developers/creating-custom-nodes.md
+++ b/docs/for-developers/creating-custom-nodes.md
@@ -1,20 +1,20 @@
# Creating Custom Nodes
-Build your own data transformation nodes with custom UI components and processing logic.
+This page is the reference for building a custom node in Python with the Node Designer API — its class structure, the UI components you can put in the settings panel, type filtering, and the `process()` contract. After reading it you can write a node that shows up in the editor with a generated settings form. For a guided end-to-end build, see the [Custom Node Tutorial](custom-node-tutorial.md).
-!!! tip "Visual Alternative"
- You can also create custom nodes visually using the [Node Designer](../users/visual-editor/node-designer.md) without writing Python files directly.
+!!! tip "Visual alternative"
+ You can also create custom nodes in the browser with the [Node Designer](../users/visual-editor/node-designer.md), without writing Python files directly.
-!!! warning "Beta Feature"
- Custom nodes are currently in beta. Some features like changing the icon are still in development.
+!!! warning "Beta feature"
+ Custom nodes are in beta. Some features (such as changing the node icon) are still in development.
-## What Are Custom Nodes?
+## What are custom nodes?
-Custom nodes let you extend Flowfile with your own data transformations that appear as native nodes in the visual editor. Each custom node includes:
+Custom nodes extend Flowfile with your own data transformations that appear alongside the built-in nodes in the visual editor. Each one has:
-- **Custom UI** - Automatically generated settings panels with dropdowns, inputs, and toggles
-- **Data Processing** - Polars-based transformation logic
-- **Visual Integration** - Appears seamlessly in the node palette
+- **A settings panel** — generated automatically from your schema (dropdowns, inputs, toggles).
+- **Processing logic** — Polars code that transforms the data.
+- **Palette placement** — under **User Defined Operations** in the node palette.
## Quick Start
@@ -29,96 +29,25 @@ Create a new Python file in your custom nodes directory:
!!! info "Custom Node Location"
The `~/.flowfile/user_defined_nodes/` directory is automatically created when you first run Flowfile. Place all your custom nodes here.
-Here's a simple example that adds a greeting column:
+Here's a simple example that adds a greeting column. Note the `process` signature — it receives one or more eager `pl.DataFrame` inputs (variadic `*inputs`) and returns a `pl.DataFrame`:
```python
-import polars as pl
-from flowfile_core.flowfile.node_designer import (
- CustomNodeBase,
- Section,
- NodeSettings,
- TextInput,
- SingleSelect,
- ColumnSelector,
- Types
-)
-
-class GreetingSettings(NodeSettings):
- main_config: Section = Section(
- title="Greeting Configuration",
- description="Configure how to greet your data",
- name_column=ColumnSelector(
- label="Name Column",
- data_types=Types.String,
- required=True
- ),
- greeting_style=SingleSelect(
- label="Greeting Style",
- options=[
- ("formal", "Formal (Hello, Mr/Ms)"),
- ("casual", "Casual (Hey there!)"),
- ("enthusiastic", "Enthusiastic (OMG HI!!!)"),
- ],
- default="casual"
- ),
- custom_message=TextInput(
- label="Custom Message",
- default="Nice to meet you!",
- placeholder="Enter your custom greeting..."
- )
- )
-
-class GreetingNode(CustomNodeBase):
- node_name: str = "Greeting Generator"
- node_category: str = "Text Processing"
- title: str = "Add Personal Greetings"
- intro: str = "Transform names into personalized greetings"
-
- settings_schema: GreetingSettings = GreetingSettings()
-
- def process(self, input_df: pl.LazyFrame) -> pl.LazyFrame:
- # Get values from the UI
- name_col = self.settings_schema.main_config.name_column.value
- style = self.settings_schema.main_config.greeting_style.value
- custom = self.settings_schema.main_config.custom_message.value
-
- # Define greeting logic
- if style == "formal":
- greeting_expr = pl.concat_str([
- pl.lit("Hello, "),
- pl.col(name_col),
- pl.lit(f". {custom}")
- ])
- elif style == "casual":
- greeting_expr = pl.concat_str([
- pl.lit("Hey "),
- pl.col(name_col),
- pl.lit(f"! {custom}")
- ])
- else: # enthusiastic
- greeting_expr = pl.concat_str([
- pl.lit("OMG HI "),
- pl.col(name_col).str.to_uppercase(),
- pl.lit(f"!!! {custom} 🎉")
- ])
-
- return input_df.with_columns([
- greeting_expr.alias("greeting")
- ])
+--8<-- "docs/examples/custom_node.py:example"
```
+This example is tested against the current API. The node-designer symbols are imported through `flowfile.node_designer` (aliased `nd` above); they are the same classes as `from flowfile_core.flowfile.node_designer import ...`.
### 2. Use Your Node
-1. **Restart Flowfile** to load your new node
-2. **Open the visual editor**
-3. **Find your node** in the "Text Processing" category
-4. **Drag it** onto the canvas
-5. **Configure the settings** in the right panel
-6. **Run your flow** and see the results!
+1. **Restart Flowfile** to load your new node.
+2. **Open the visual editor.**
+3. **Find your node** under **User Defined Operations** in the node palette — every custom node lands there (see [The palette section](#the-palette-section)).
+4. **Drag it** onto the canvas.
+5. **Configure the settings** in the right panel.
+6. **Run your flow.**
- Visual overview of the result!
+Visual overview of the result

@@ -133,41 +62,35 @@ Every custom node has three main parts:
```python
class MyCustomNode(CustomNodeBase):
- # 1. Metadata - How the node appears in Flowfile
- node_name: str = "My Amazing Node"
+ # 1. Metadata - how the node appears in Flowfile
+ node_name: str = "My Node"
node_category: str = "Data Enhancement"
- title: str = "Add Personal Greetings"
- intro: str = "Transform names into personalized greetings"
+ title: str = "Add greetings"
+ intro: str = "Prefix a name column with a greeting."
- # 2. Settings Schema - The UI configuration
+ # 2. Settings schema - the UI configuration
settings_schema: MySettings = MySettings()
-
- # 3. Processing Logic - What the node actually does
- def process(self, input_df: pl.LazyFrame) -> pl.LazyFrame:
- # Your transformation logic here
- return modified_df
+
+ # 3. Processing logic - what the node does
+ def process(self, *inputs: pl.DataFrame) -> pl.DataFrame:
+ df = inputs[0]
+ # ... transformation logic ...
+ return df
```
#### 3. Process
-!!! Warning "Only one input supported for now"
- Currently, custom nodes only support a single input DataFrame. Support for multiple inputs is planned for future releases.
-
-
-The process method is the engine of your node. This is where you write your Polars code to transform the data.
-
-- Input: The method receives the incoming data as a Polars LazyFrame. If your node has multiple inputs, they will be passed as separate arguments (e.g., process(self, df1)).
-
-- Accessing Settings: Inside this method, you can get the current values from your UI components using self.settings_schema...value.
-
-- Output: The method must return a single Polars LazyFrame, which becomes the output of your node.
-
-
-
+The `process` method is the engine of your node — where you write Polars code to transform the data.
+- **Input:** the method receives its inputs as eager `pl.DataFrame` objects, passed as a variadic `*inputs`. A single-input node reads `inputs[0]`; a node with multiple inputs indexes further.
+- **Accessing settings:** read current UI values with `self.settings_schema...value`.
+- **Output:** return a single `pl.DataFrame` (or, for a multi-output node, a `dict[str, pl.DataFrame]` keyed by output name).
+### The palette section
+Every custom node appears in the node palette under one section: **User Defined Operations**. That placement is driven by `node_group`, which defaults to `"custom"` on `CustomNodeBase` — you don't need to set it.
+`node_category` is separate, descriptive metadata (shown in the node header and browser modal). It does **not** create or select a palette section, so a value like `"Text Processing"` or `"Data Validation"` organizes nothing in the palette — all custom nodes still sit under User Defined Operations.
### Settings Architecture
@@ -285,20 +208,25 @@ text_columns = ColumnSelector(
```
### Secret Selector
-Access stored secrets (API keys, credentials, tokens) securely:
+Access stored secrets (API keys, credentials, tokens). `SecretSelector`, like every other component, is added inside a `Section` as a keyword argument (the keyword is the field name). It has no `name=` field — only `label`:
```python
-from flowfile_core.flowfile.node_designer import SecretSelector
+from flowfile_core.flowfile.node_designer import (
+ CustomNodeBase, NodeSettings, Section, SecretSelector
+)
-class MyNode(CustomNodeBase):
- settings_schema = NodeSettings(
- components=[
- SecretSelector(name="api_key", label="API Key"),
- ]
+class ApiSettings(NodeSettings):
+ connection: Section = Section(
+ title="Connection",
+ api_key=SecretSelector(label="API Key"),
)
+
+class MyNode(CustomNodeBase):
+ node_name: str = "API Reader"
+ settings_schema: ApiSettings = ApiSettings()
```
-The SecretSelector displays a dropdown of available secrets configured by the user. Secrets are stored securely and retrieved at runtime. This is useful for nodes that connect to external APIs or services.
+The dropdown lists the secrets configured by the current user. Read the decrypted value inside `process()` with `self.settings_schema.connection.api_key.secret_value` — it is only accessible during execution. This is useful for nodes that connect to external APIs or services.
### Dynamic Column Options
Use `IncomingColumns` for dropdowns that populate with input columns:
@@ -320,17 +248,18 @@ from flowfile_core.flowfile.node_designer import Types
# Type groups
Types.Numeric # All numeric types
Types.String # String and categorical
-Types.Date # Date, datetime, time
+Types.AnyDate # Date, datetime, time, duration
Types.Boolean # Boolean columns
Types.All # All column types
# Specific types
Types.Int64 # 64-bit integers
Types.Float # Float64
-Types.Decimal # Decimal types
+Types.Decimal # Decimal type
+Types.Date # The Date type specifically (not the date group)
# Mix and match
-data_types=[Types.Numeric, Types.Date] # Numbers and dates only
+data_types=[Types.Numeric, Types.AnyDate] # Numbers and dates only
```
## Real-World Examples
@@ -341,8 +270,8 @@ data_types=[Types.Numeric, Types.Date] # Numbers and dates only
class DataQualityNode(CustomNodeBase):
node_name: str = "Data Quality Checker"
node_category: str = "Data Validation"
-
- settings_schema: DataQualitySettings = DataQualitySettings(
+
+ settings_schema: NodeSettings = NodeSettings(
validation_rules=Section(
title="Validation Rules",
columns_to_check=ColumnSelector(
@@ -363,29 +292,21 @@ class DataQualityNode(CustomNodeBase):
)
)
- def process(self, input_df: pl.LazyFrame) -> pl.LazyFrame:
+ def process(self, *inputs: pl.DataFrame) -> pl.DataFrame:
+ df = inputs[0]
columns = self.settings_schema.validation_rules.columns_to_check.value
threshold = self.settings_schema.validation_rules.null_threshold.value
-
- # Calculate quality metrics
- quality_checks = []
+
+ row_count = df.height
+ result = df
for col in columns:
- null_pct = (input_df[col].is_null().sum() / len(input_df)) * 100
- quality_checks.append({
- "column": col,
- "null_percentage": null_pct,
- "quality_flag": "PASS" if null_pct <= threshold else "FAIL"
- })
-
- # Add quality flags to original data
- result_df = input_df
- for check in quality_checks:
- if check["quality_flag"] == "FAIL":
- result_df = result_df.with_columns([
- pl.col(check["column"]).is_null().alias(f"{check['column']}_has_issues")
- ])
-
- return result_df
+ null_pct = (df[col].null_count() / row_count) * 100 if row_count else 0.0
+ if null_pct > threshold:
+ # Flag the failing column with a per-row null indicator
+ result = result.with_columns(
+ pl.col(col).is_null().alias(f"{col}_has_issues")
+ )
+ return result
```
### Text Processing Node
@@ -394,8 +315,8 @@ class DataQualityNode(CustomNodeBase):
class TextCleanerNode(CustomNodeBase):
node_name: str = "Text Cleaner"
node_category: str = "Text Processing"
-
- settings_schema: TextCleanerSettings = TextCleanerSettings(
+
+ settings_schema: NodeSettings = NodeSettings(
cleaning_options=Section(
title="Cleaning Options",
text_column=ColumnSelector(
@@ -421,14 +342,15 @@ class TextCleanerNode(CustomNodeBase):
)
)
- def process(self, input_df: pl.LazyFrame) -> pl.LazyFrame:
+ def process(self, *inputs: pl.DataFrame) -> pl.DataFrame:
+ df = inputs[0]
text_col = self.settings_schema.cleaning_options.text_column.value
operations = self.settings_schema.cleaning_options.operations.value
output_col = self.settings_schema.cleaning_options.output_column.value
-
- # Start with original text
+
+ # Start with the original text
expr = pl.col(text_col)
-
+
# Apply selected operations
if "lowercase" in operations:
expr = expr.str.to_lowercase()
@@ -440,48 +362,33 @@ class TextCleanerNode(CustomNodeBase):
expr = expr.str.replace_all(r"\d+", "")
if "trim" in operations:
expr = expr.str.strip_chars()
-
- return input_df.with_columns([expr.alias(output_col)])
-```
-## Best Practices
+ return df.with_columns(expr.alias(output_col))
+```
-### 1. Performance
-Try to use Polars expressions and lazy evaluation to keep your nodes efficient.
-A collect will be executed in the core process and can cause issues when using remote compute.
+## Performance note
+Express transformations as Polars expressions on the incoming `pl.DataFrame` rather than iterating rows in Python. `process()` runs against an eager DataFrame, so keep per-row Python work (like `map_elements`) to cases that genuinely need it.
## Troubleshooting
-### Node Doesn't Appear
-1. Check the file is in `~/.flowfile/user_defined_nodes/`
-2. Restart Flowfile completely
-3. Check for Python syntax errors in terminal
-4. Ensure your class inherits from `CustomNodeBase`
-
-### Settings Don't Work
-1. Verify `settings_schema` is properly assigned
-2. Check component imports
-3. Ensure section structure is correct
-4. Use `.value` to access component values in `process()`
-
-### Processing Errors
-1. Check input DataFrame exists and has expected columns
-2. Use `print()` or logging for debugging
-3. Handle null values and edge cases
-4. Ensure return type is `pl.LazyFrame`
-
-
-## Coming Soon
+### Node doesn't appear
+1. Check the file is in `~/.flowfile/user_defined_nodes/`.
+2. Restart Flowfile completely.
+3. Check for Python syntax errors in the terminal.
+4. Ensure your class inherits from `CustomNodeBase`.
+5. Look under **User Defined Operations** in the palette — not a section named after your `node_category`.
-The following features are planned for future releases:
+### Settings don't work
+1. Verify `settings_schema` is assigned.
+2. Check component imports.
+3. Ensure the section structure is correct (components are keyword arguments inside a `Section`).
+4. Use `.value` to read component values in `process()`.
-- **Node Templates** - Quick-start templates for common patterns
-- **Custom Icons** - Upload custom icons for your nodes
-- **Node Categories** - Create your own node categories
-- **Testing Framework** - Built-in testing for custom nodes
-- **Node Publishing** - Share nodes with the community
+### Processing errors
+1. Check the input DataFrame has the expected columns.
+2. Ensure `process()` returns a `pl.DataFrame`.
---
-Ready to build? Start with the [Custom Node Tutorial](custom-node-tutorial.md) for a step-by-step walkthrough!
\ No newline at end of file
+For a step-by-step walkthrough, see the [Custom Node Tutorial](custom-node-tutorial.md).
\ No newline at end of file
diff --git a/docs/for-developers/custom-node-tutorial.md b/docs/for-developers/custom-node-tutorial.md
index a348fad9e..bf8ed738f 100644
--- a/docs/for-developers/custom-node-tutorial.md
+++ b/docs/for-developers/custom-node-tutorial.md
@@ -1,13 +1,14 @@
# Custom Node Tutorial: Build an Emoji Generator
-Learn to create custom nodes by building a fun emoji generator that transforms boring data into expressive datasets with visual flair.
+This tutorial builds one custom node end to end — an Emoji Generator that maps numeric values to emojis. It is a small but complete example of the [Node Designer](creating-custom-nodes.md) API: a multi-section settings schema, type-filtered column selection, and a `process()` method that transforms an incoming DataFrame.
-!!! info "What You'll Build"
- By the end of this tutorial, you'll have created a fully functional "Emoji Generator" node that:
- - Takes numeric data and converts it into mood-based emojis
- - Provides 7 different emoji themes (performance, temperature, money, etc.)
- - Includes intensity controls and random sparkle effects
- - Features a sophisticated multi-section UI
+!!! info "What you'll build"
+ By the end you will have an "Emoji Generator" node that:
+
+ - Maps a numeric column to mood-based emojis.
+ - Offers seven emoji themes (performance, temperature, money, and so on).
+ - Includes intensity controls and an optional random sparkle.
+ - Uses a two-section settings panel.
## Prerequisites
@@ -156,10 +157,10 @@ class EmojiSettings(NodeSettings):
class EmojiGenerator(CustomNodeBase):
# Node metadata - how it appears in Flowfile
node_name: str = "Emoji Generator 🎉"
- node_category: str = "Fun Stuff" # This creates a new category
- node_group: str = "custom"
+ node_category: str = "Fun Stuff" # descriptive label only; does NOT create a palette section
+ node_group: str = "custom" # node_group="custom" puts it under "User Defined Operations"
title: str = "Emoji Generator"
- intro: str = "Transform boring data into fun emoji-filled datasets! 🚀"
+ intro: str = "Add an emoji column derived from a numeric column."
# I/O configuration
number_of_inputs: int = 1
@@ -168,17 +169,18 @@ class EmojiGenerator(CustomNodeBase):
# Link to our settings schema
settings_schema: EmojiSettings = EmojiSettings()
- def process(self, input_df: pl.DataFrame) -> pl.DataFrame:
- # We'll implement this in the next step
- pass
+ def process(self, *inputs: pl.DataFrame) -> pl.DataFrame:
+ # Implemented in the next step
+ ...
```
## Step 5: Implement the Processing Logic
-Now for the fun part - the actual emoji generation logic:
+Now the processing logic — mapping each value to an emoji. The method receives its inputs as a variadic `*inputs`; this node has one input, so it reads `inputs[0]`:
```python
-def process(self, input_df: pl.DataFrame) -> pl.DataFrame:
+def process(self, *inputs: pl.DataFrame) -> pl.DataFrame:
+ input_df = inputs[0]
# Get settings values from the UI
column_name = self.settings_schema.mood_config.source_column.value
mood_type = self.settings_schema.mood_config.mood_type.value
@@ -298,285 +300,46 @@ Save your file and test it:
flowfile run ui
```
-2. **Create a test flow**:
- - Create a new flow
- - Add a "Manual Input" node with some test data:
+2. **Create a test flow.**
+ - Create a new flow.
+ - Add a "Manual Input" node. The `source_column` selector filters to numeric columns, and the threshold logic compares numbers, so use numeric `value` entries (not quoted strings):
```
[
- {
- "name": "bob",
- "value": "21"
- },
- {
- "name": "magret",
- "value": "62.1"
- },
- {
- "name": "fish",
- "value": "1.2"
- },
- {
- "name": "dog",
- "value": "20"
- }
+ {"name": "bob", "value": 21},
+ {"name": "magret", "value": 62.1},
+ {"name": "fish", "value": 1.2},
+ {"name": "dog", "value": 20}
]
```
- - Look for your "Emoji Generator 🎉" in the "User defined operations" category
- - Connect it to your manual input
- - Configure the settings and run!
+ - Find your "Emoji Generator" under **User Defined Operations** in the palette.
+ - Connect it to your manual input.
+ - Configure the settings and run.
- Visual overview of the result!
+Visual overview of the result

-### Performance Tips
+### Performance notes
-1. **Use Polars expressions** instead of Python loops when possible
-2. **Avoid collecting DataFrames** in the middle of processing
-3. **Handle large datasets** by checking input size and warning users
-4. **Cache expensive operations** like random number generation
-5. **Use lazy evaluation** - let Polars optimize the query plan
+1. Prefer Polars expressions over Python loops where the logic allows it.
+2. `map_elements` (used here) runs a Python callback per row — fine for a small demo, slower on large data.
+3. Read settings once at the top of `process()` rather than inside the row callback.
## Complete Working Example
-Here's the complete, working emoji generator node:
-
-```python
-import polars as pl
-import random
-from typing import List
-
-from flowfile_core.flowfile.node_designer import (
- CustomNodeBase,
- Section,
- NodeSettings,
- TextInput,
- NumericInput,
- SingleSelect,
- ToggleSwitch,
- ColumnSelector,
- MultiSelect,
- Types
-)
-
-
-class EmojiMoodSection(Section):
- source_column: ColumnSelector = ColumnSelector(
- label="Analyze This Column",
- multiple=False,
- required=True,
- data_types=Types.Numeric # Only show numeric columns
- )
-
- mood_type: SingleSelect = SingleSelect(
- label="Emoji Mood Logic",
- options=[
- ("performance", "📈 Performance Based (High = 😎, Low = 😰)"),
- ("temperature", "🌡️ Temperature (Hot = 🔥, Cold = 🧊)"),
- ("money", "💰 Money Mode (Rich = 🤑, Poor = 😢)"),
- ("energy", "⚡ Energy Level (High = 🚀, Low = 🔋)"),
- ("love", "❤️ Love Meter (High = 😍, Low = 💔)"),
- ("chaos", "🎲 Pure Chaos (Random emojis!)"),
- ("pizza", "🍕 Pizza Scale (Everything becomes pizza)")
- ],
- default="performance"
- )
-
- threshold_value: NumericInput = NumericInput(
- label="Mood Threshold",
- default=50.0,
- min_value=0,
- max_value=100
- )
-
- emoji_column_name: TextInput = TextInput(
- label="New Emoji Column Name",
- default="mood_emoji",
- placeholder="Name your emoji column..."
- )
-
-
-class EmojiStyleSection(Section):
- emoji_intensity: SingleSelect = SingleSelect(
- label="Emoji Intensity",
- options=[
- ("subtle", "😐 Subtle (One emoji)"),
- ("normal", "😊 Normal (1-2 emojis)"),
- ("extra", "🤩 Extra (2-3 emojis)"),
- ("maximum", "🤯🎉🚀 MAXIMUM OVERDRIVE")
- ],
- default="normal"
- )
-
- add_random_sparkle: ToggleSwitch = ToggleSwitch(
- label="Add Random Sparkles ✨",
- default=True,
- description="Randomly sprinkle ✨ for extra pizzazz"
- )
-
- emoji_categories: MultiSelect = MultiSelect(
- label="Allowed Emoji Categories",
- options=[
- ("faces", "😀 Faces & Emotions"),
- ("animals", "🦄 Animals"),
- ("food", "🍔 Food & Drink"),
- ("nature", "🌈 Nature"),
- ("objects", "🎮 Objects"),
- ("symbols", "💯 Symbols"),
- ("flags", "🏴☠️ Flags")
- ],
- default=["faces", "animals", "food"]
- )
-
-
-class EmojiSettings(NodeSettings):
- mood_config: EmojiMoodSection = EmojiMoodSection(
- title="Mood Detection 😊",
- description="Configure how to detect the vibe of your data"
- )
-
- style_options: EmojiStyleSection = EmojiStyleSection(
- title="Emoji Style 🎨",
- description="Fine-tune your emoji experience"
- )
-
-
-class EmojiGenerator(CustomNodeBase):
- # Node metadata - how it appears in Flowfile
- node_name: str = "Emoji Generator 🎉"
- node_category: str = "Fun Stuff" # This creates a new category
- node_group: str = "custom"
- title: str = "Emoji Generator"
- intro: str = "Transform boring data into fun emoji-filled datasets! 🚀"
-
- # I/O configuration
- number_of_inputs: int = 1
- number_of_outputs: int = 1
-
- # Link to our settings schema
- settings_schema: EmojiSettings = EmojiSettings()
-
- def process(self, input_df: pl.DataFrame) -> pl.DataFrame:
- # Get settings values from the UI
- # Note how we can just access these from the settings that we have defined
- column_name = self.settings_schema.mood_config.source_column.value
- mood_type = self.settings_schema.mood_config.mood_type.value
- threshold = self.settings_schema.mood_config.threshold_value.value
- emoji_col_name = self.settings_schema.mood_config.emoji_column_name.value
- intensity = self.settings_schema.style_options.emoji_intensity.value
- add_sparkle = self.settings_schema.style_options.add_random_sparkle.value
-
- # Define emoji sets for different moods
- emoji_sets = {
- "performance": {
- "high": ["😎", "💪", "🏆", "👑", "🌟", "💯", "🔥"],
- "low": ["😰", "😓", "📉", "😢", "💔", "🆘", "😵"]
- },
- "temperature": {
- "high": ["🔥", "🌋", "☀️", "🥵", "🌡️", "♨️", "🏖️"],
- "low": ["🧊", "❄️", "⛄", "🥶", "🌨️", "🏔️", "🐧"]
- },
- "money": {
- "high": ["🤑", "💰", "💎", "🏦", "💳", "🪙", "📈"],
- "low": ["😢", "💸", "📉", "🏚️", "😭", "🥺", "📊"]
- },
- "energy": {
- "high": ["🚀", "⚡", "💥", "🎯", "🏃", "🤸", "🎪"],
- "low": ["🔋", "😴", "🛌", "🐌", "🥱", "😪", "💤"]
- },
- "love": {
- "high": ["😍", "❤️", "💕", "🥰", "💘", "💝", "👨❤️👨"],
- "low": ["💔", "😢", "😭", "🥀", "😔", "💀", "🖤"]
- },
- "chaos": {
- "high": ["🦖", "🎸", "🚁", "🎪", "🦜", "🎭", "🏴☠️"],
- "low": ["🥔", "🧦", "📎", "🦷", "🧲", "🔌", "🪣"]
- },
- "pizza": {
- "high": ["🍕", "🍕🍕", "🍕🔥", "🍕😍", "🍕🎉", "🍕💯", "🍕👑"],
- "low": ["🍕", "🍕😢", "🍕💔", "🍕😭", "🍕🥺", "🍕😔", "🍕"]
- }
- }
-
- # Helper function to get emoji based on value
- def get_emoji(value, mood_type, threshold, intensity):
- if value is None:
- return "❓"
-
- emoji_list = emoji_sets.get(mood_type, emoji_sets["performance"])
-
- if mood_type == "chaos":
- # Random emoji from both lists
- all_emojis = emoji_list["high"] + emoji_list["low"]
- base_emoji = random.choice(all_emojis)
- elif mood_type == "pizza":
- # Everything is pizza
- base_emoji = "🍕"
- else:
- # Use threshold to determine high/low
- if value >= threshold:
- base_emoji = random.choice(emoji_list["high"])
- else:
- base_emoji = random.choice(emoji_list["low"])
-
- # Add intensity
- if intensity == "subtle":
- result = base_emoji
- elif intensity == "normal":
- result = base_emoji
- if random.random() > 0.5:
- result += random.choice(["", "✨", ""])
- elif intensity == "extra":
- extras = ["✨", "💫", "⭐", ""]
- result = base_emoji + random.choice(extras) + random.choice(extras)
- else: # maximum
- chaos_emojis = ["🎉", "🚀", "💥", "🌈", "✨", "🔥", "💯", "⚡"]
- result = base_emoji + "".join(random.choices(chaos_emojis, k=3))
-
- # Add random sparkle
- if add_sparkle and random.random() > 0.7:
- result += "✨"
-
- return result
-
- # Create emoji column using map_elements
- emoji_expr = (
- pl.col(column_name)
- .map_elements(
- lambda x: get_emoji(x, mood_type, threshold, intensity),
- return_dtype=pl.String
- )
- .alias(emoji_col_name)
- )
-
- # Add bonus columns based on intensity
- if intensity == "maximum":
- # Add extra fun columns in maximum mode
- return input_df.with_columns([
- emoji_expr,
- pl.lit("🎉 PARTY MODE 🎉").alias("vibe_check"),
- pl.col(column_name).map_elements(
- lambda x: "🔥" * min(int((x or 0) / 20), 5) if x else "💤",
- return_dtype=pl.String
- ).alias("fire_meter")
- ])
- else:
- return input_df.with_columns([emoji_expr])
-```
+The complete node is the assembly of Steps 3–5: the two `Section` classes and `EmojiSettings` from Step 3, the `EmojiGenerator` class from Step 4, and its `process()` body from Step 5, in one file.
-Save this as `~/.flowfile/user_defined_nodes/emoji_generator.py`, restart Flowfile, and enjoy your new emoji-powered data transformations!
+Save that assembled file as `~/.flowfile/user_defined_nodes/emoji_generator.py`, then restart Flowfile.
-## Congratulations! 🎉
+!!! note "One setting is defined but unused"
+ `emoji_categories` (the `MultiSelect` in the style section) is collected by the UI but never read in `process()`. It's left in as an exercise — wire it into `get_emoji` to restrict which emoji pools a theme draws from.
-You've successfully created a fully functional custom node
+## Recap
-- ✅ Multi-section UI with 6 different component types
-- ✅ Complex processing logic with multiple mood themes
-- ✅ Advanced features like intensity control and random effects
-- ✅ Professional documentation and structure
+You built a custom node with a two-section settings panel, type-filtered column selection, and a `process()` method that maps values to emojis. See [Creating Custom Nodes](creating-custom-nodes.md) for the full component catalog and the `SecretSelector` for API-backed nodes.
diff --git a/docs/for-developers/design-philosophy.md b/docs/for-developers/design-philosophy.md
index e79bc84e7..2c2dde913 100644
--- a/docs/for-developers/design-philosophy.md
+++ b/docs/for-developers/design-philosophy.md
@@ -1,47 +1,28 @@
-# The Architecture Story: How Flowfile Bridges Code and Visual Worlds
+# Design Philosophy: Code and Visual Are One Model
-📋 TL;DR - Key Takeaways
+This page explains the core design decision behind Flowfile: the Python API and the visual editor construct the same underlying objects. It is for contributors mapping the codebase, and for anyone curious how a drag-and-drop tool and a code API can stay in lockstep.
-!!! abstract "Key Points"
- - **Two ways to build pipelines**: Write Python code or use drag-and-drop UI - both create the same thing
- - **Settings-based design**: Every transformation is just a configuration object (Pydantic model)
- - **Clear separation**: Graph structure, settings, and execution are handled separately
- - **Happy accident**: Started as a UI project, ended up with an architecture that works great for both UI and code
+!!! info "Related pages"
+ - [Technical Architecture](architecture.md) — the three services and execution modes
+ - [Core Developer Guide](flowfile-core.md) — internal implementation, hands-on
+ - [Python API](../users/python-api/index.md) — using Flowfile in your own projects
+## The problem
-!!! info "Navigation"
- - **This page**: Architecture overview and design decisions
- - **[Core Developer Guide](flowfile-core.md)**: Technical implementation details
- - **[Python API](../users/python-api/index.md)**: How to use Flowfile in your projects
+Most data tools make you choose: a visual interface (approachable but limited) or code (expressive but harder). Flowfile aims for both in one tool, which raises one hard question — how do you make a drag-and-drop interface produce the exact same pipelines as writing code?
-👥 Who Should Read This?
-!!! question "Target Audience"
- - Contributors who want to understand the codebase
- - Users curious about how things work internally
- - Developers building similar dual-interface tools
- - Anyone interested in bridging UI and code approaches
-
-## The Problem We Solved
-
-Most data tools force you to choose: either use a visual interface (easy but limited) or write code (powerful but complex). We wanted both in the same tool.
-
-The challenge: How do you make a visual drag-and-drop interface that creates the exact same pipelines as writing code?
-
-The platform started with a clean, settings-based backend where every transformation is a declarative configuration object. This design is perfect for a UI. But developers don't think in configuration objects—they think in code:
+The backend was built around a settings-based model: every transformation is a declarative configuration object (a Pydantic model). That model suits a UI well, but developers think in code, not configuration objects:
```python
# How developers want to write data code
df.filter(col("price") > 100).group_by("region").sum()
```
-The breakthrough came from realizing that the Polars API would be able to convert to our settings object and therefore creating the same settings object that the UI creates. Both interfaces become different ways to build the same underlying configuration, giving developers the expressiveness they want while maintaining the structured settings the UI needs.
+The resolution is to have the Python API build those same settings objects. Both interfaces become different front-ends to one underlying configuration: developers get an expressive, chainable API, and the UI gets the structured settings it needs.
## The result
-!!! abstract "How It Actually Happened"
- This wasn't some grand plan. I started building a drag-and-drop UI and needed a clean way to configure nodes. Settings objects made sense for the UI. But the development of Flowfile has never been a planned approach, it was just about building what sounded _fun_.
- Later, when looking at other projects, I realized I could just have the API methods create the same settings objects, well that is **_fun_**. Suddenly there were two equivalent interfaces almost by accident,.
- Since Polars does the actual data processing, our settings just configure what Polars should do. This turned out to be an easy abstraction layer that showed it's potential from the start.
+Flowfile grew out of a visual editor. Nodes were configured with settings objects because that is a clean fit for a UI. Later, the Python API was wired to construct those same settings objects, so a method call and a dropped node produce identical configuration. Because Polars does the actual data processing, the settings only describe *what* Polars should do — a thin, stable abstraction layer.
The result is a Python API that constructs the exact same configuration objects as the visual editor:
@@ -52,7 +33,7 @@ Both interfaces are different ways to build the same Directed Acyclic Graph (DAG
## One Pipeline, Two Ways
-Let's build the same pipeline using both approaches to see how they produce identical results.
+The same pipeline built both ways produces identical results.
### Sample Data
@@ -101,29 +82,35 @@ df_4 = df_3.group_by(['region']).agg([
**Graph Introspection:**
```python
-# Access all nodes that were created in the graph
+# Access all nodes that were created in the graph.
+# Node ids come from a global incrementing counter (generate_node_id), so
+# the exact numbers depend on how many ids were minted before this run.
print(graph._node_db)
-# {1: Node id: 1 (manual_input),
-# 3: Node id: 3 (formula),
-# 4: Node id: 4 (filter),
-# 5: Node id: 5 (group_by)}
+# {: Node id: (manual_input),
+# : Node id: (formula),
+# : Node id: (filter),
+# : Node id: (group_by)}
# Find the starting node(s) of the graph
print(graph._flow_starts)
-# [Node id: 1 (manual_input)]
+# [Node id: (manual_input)]
+
+# Each FlowFrame exposes the node id it produced
+manual_id = df_1.node_id
+formula_id = df_2.node_id
# From every node, access the next node that depends on it
-print(graph.get_node(1).leads_to_nodes)
-# [Node id: 3 (formula)]
+print(graph.get_node(manual_id).leads_to_nodes)
+# [Node id: (formula)]
# The other way around works too
-print(graph.get_node(3).node_inputs)
-# NodeStepInputs(Left Input: None, Right Input: None,
-# Main Inputs: [Node id: 1 (manual_input)])
+print(graph.get_node(formula_id).node_inputs)
+# NodeStepInputs(Left Input: None, Right Input: None,
+# Main Inputs: [Node id: (manual_input)])
# Access the settings and type of any node
-print(graph.get_node(4).setting_input)
-print(graph.get_node(4).node_type)
+print(graph.get_node(formula_id).setting_input)
+print(graph.get_node(formula_id).node_type)
```
@@ -224,7 +211,7 @@ This is the polars query plan generated by both methods:
#### 1. The DAG is Everything
-Every Flowfile pipeline is a Directed Acyclic Graph where. This is captured in the [FlowGraph](python-api-reference.md#flowgraph)
+Every Flowfile pipeline is a Directed Acyclic Graph where nodes are operations and edges are data dependencies, captured in the [FlowGraph](python-api-reference.md#flowgraph):
- **Nodes** are transformations (filter, join, group_by, etc.)
- **Edges** represent data flow between nodes
@@ -232,7 +219,7 @@ Every Flowfile pipeline is a Directed Acyclic Graph where. This is captured in t
#### 2. Settings Drive Everything
-Every node is composed of two parts: the **Node class** (a Pydantic BaseModel) that holds metadata and the **Settings** (often dataclasses) that configure the transformation:
+Every node is composed of two parts: the **Node class** (a Pydantic BaseModel) that holds metadata, and the **Settings** (also Pydantic BaseModels) that configure the transformation:
Read more about [Nodes](python-api-reference.md#input_schema) and the [transformations](python-api-reference.md#transform_schema)
@@ -253,67 +240,41 @@ class NodeBase(BaseModel):
description: Optional[str] = None
# ... graph metadata ...
-# The Settings: transformation configuration (dataclass)
-@dataclass
-class GroupByInput:
+# The Settings: transformation configuration (Pydantic BaseModel
+# with a custom __init__ that also accepts positional args)
+class GroupByInput(BaseModel):
"""Defines how to perform the group by operation"""
agg_cols: List[AggColl]
-@dataclass
-class AggColl:
+class AggColl(BaseModel):
"""Single aggregation operation"""
- old_name: str # Column to aggregate
- agg: str # Aggregation function ('sum', 'mean', etc.)
+ old_name: str # Column to aggregate
+ agg: str # Aggregation function ('sum', 'mean', etc.)
new_name: Optional[str] # Output column name
output_type: Optional[str] = None
```
-!!! tip "Settings Power The Backend"
- This dual structure—Nodes for graph metadata, Settings for transformation logic—drives the backend:
-
- - 🔧 **Code generation** (method signatures match settings)
- - 💾 **Serialization** (graphs can be saved/loaded)
- - 🔮 **Schema prediction** (output types are inferred from AggColl)
- - 🎨 **UI structure** (defines what the frontend needs to collect, though forms are manually built)
+!!! tip "Settings power the backend"
+ This dual structure — Nodes for graph metadata, Settings for transformation logic — drives the backend:
+
+ - **Code generation** — method signatures match settings.
+ - **Serialization** — graphs can be saved and loaded.
+ - **Schema prediction** — output types are inferred from settings like `AggColl`.
+ - **UI structure** — defines what the frontend collects (native-node forms are hand-built; custom nodes are auto-generated).
#### 3. Execution is Everything
-The [`FlowDataEngine`](python-api-reference.md#flowfile_core.flowfile.flow_data_engine.flow_data_engine.FlowDataEngine) orchestrates everything about execution. While the DAG defines structure and settings define configuration, FlowDataEngine is the runtime brain that makes it all happen.
+The [`FlowDataEngine`](python-api-reference.md#flowfile_core.flowfile.flow_data_engine.flow_data_engine.FlowDataEngine) owns execution. While the DAG defines structure and settings define configuration, FlowDataEngine wraps a Polars LazyFrame/DataFrame and decides where, when, and how transformations run:
-FlowDataEngine handles:
- **Compute location** (worker service vs local execution)
- **Caching strategy** (when to materialize, where to store)
- **Schema caching** (avoiding redundant schema calculations)
- **Lazy vs eager evaluation** (performance vs debugging modes)
- **Data movement** (passing LazyFrames between transformations)
-This separation is powerful: the DAG remains a pure specification, settings stay declarative, and FlowDataEngine owns all execution concerns. It wraps a Polars LazyFrame/DataFrame but is really the execution orchestrator—deciding where, when, and how transformations run.
-
### Understanding FlowNode
-The `FlowNode` class is the heart of each transformation in the graph. Each node encapsulates everything needed for a single transformation step:
-
-!!! info "Core FlowNode Components"
- **Essential State:**
-
- - **`_function`**: The closure containing the transformation logic
- - **`leads_to_nodes`**: List of downstream nodes that depend on this one
- - **`node_information`**: Metadata (id, type, position, connections)
- - **`_hash`**: Unique identifier based on settings and parent hashes
-
- **Runtime State:**
-
- - **`results`**: Holds the resulting data, errors, and example data paths
- - **`node_stats`**: Tracks execution status (has_run, is_canceled, etc.)
- - **`node_settings`**: Runtime settings (cache_results, streamable, etc.)
- - **`state_needs_reset`**: Flag indicating if the node needs recalculation
-
- **Schema Information:**
-
- - **`node_schema`**: Input/output columns and predicted schemas
- - **`schema_callback`**: Function to calculate schema without execution
-
-The beauty is that FlowNode doesn't know about specific transformations—it just orchestrates the execution of its `_function` closure with the right inputs and manages the resulting state.
+Each `FlowNode` wraps a single transformation: it holds the `_function` closure that carries the node's logic, its downstream links, hash, runtime state, and schema. FlowNode doesn't know about specific transformations — it orchestrates its `_function` closure with the right inputs and manages the resulting state. For the full component-by-component walkthrough, see the [Core Developer Guide](flowfile-core.md).
## Flowfile: The Use of Closures
@@ -358,7 +319,7 @@ graph LR
func4 ==> |Final FlowDataEngine with full LazyFrame plan| Result
```
-Each `_func` is a closure that wraps around the previous one, building up a chain. The beauty is that Polars can track the schema through this entire chain without executing any data transformations—it just builds the query plan!
+Each `_func` is a closure that wraps around the previous one, building up a chain. Polars tracks the schema through this entire chain without executing any data transformations — it just builds the query plan.
#### The Closure Pattern in Practice
@@ -411,14 +372,6 @@ print(result.data_frame.collect_schema())
# Schema([('region', String), ('total_revenue', Float64), ('avg_transaction', Float64)])
```
-!!! example "Why This Works"
- 1. **Each `_func` is a closure** containing the node's settings
- 2. **Functions only need FlowDataEngine as input** (or multiple for joins/unions)
- 3. **LazyFrame tracks schema changes** through the entire chain
- 4. **No data is processed**—Polars just builds the query plan
-
- The result: instant schema feedback without running expensive computations!
-
### Fallback: Schema Callbacks
For nodes that can't infer schemas automatically (external data sources), each FlowNode can have a `schema_callback`:
@@ -430,187 +383,97 @@ def schema_callback(settings, input_schema):
return new_schema
```
-## Execution Methods
-
-Flowfile offers flexible execution strategies depending on your needs:
+## Running a flow
-### 🚀 Available Execution Methods
+The two execution modes — **Development** and **Performance** — are described in full on the [Technical Architecture page](architecture.md#execution-modes). The code below shows how each is invoked and how to read the query plan.
-#### Performance Mode
-
-**When to use:** Production pipelines, large datasets
+**Performance mode.** Pull the final result; Polars optimizes and runs the whole pipeline once, with no intermediate materialization.
```python
-# Get the final result efficiently
result = flow.get_node(final_node_id).get_resulting_data()
```
-**Characteristics:**
-
-- ⚡ Pull-based execution from the final node
-- 🎯 Polars optimizes the entire pipeline
-- 💨 Data flows once through optimized plan
-- 🚫 No intermediate materialization
-
-#### Development Mode
-
-**When to use:** Debugging, inspection, incremental development
+**Development mode.** Push-based execution in topological order; each node's output is written to disk so any intermediate result can be inspected. On re-run, only changed nodes (and their descendants) run again.
```python
-# Execute with caching enabled
import flowfile as ff
flow = ff.create_flow_graph()
flow.flow_settings.execution_mode = "Development"
-# Add transformations here
+# ... add transformations ...
flow.run_graph()
-# Inspect intermediate results
-node_3_result = flow.get_node(3).results.get_example_data()
-
-flow.get_node(3).needs_run(performance_mode=False) # False
-
+# Inspect an intermediate result by node id
+node = flow.get_node(some_node_id)
+example = node.results.get_example_data()
+node.needs_run(performance_mode=False) # False once it has run
```
-**Characteristics:**
-- 📝 Push-based execution in topological order
-- 💾 Each node's output written to disk
-- 🔍 Inspect any intermediate result
-- 🔄 When re-running the flow, only the steps that have changed(directly and indirectly) will run
-
-#### Explain Plan
-
-**When to use:** Optimization, understanding deeply the execution plan.
-
-!!! warning This feature uses directly the Polars implementation, when the full flow cannot be fully converted to Polars, it will show partial executions.
+**Explain plan.** See the optimized Polars plan without executing.
```python
-# See what Polars will actually do
plan = flow.get_node(node_id).get_resulting_data().data_frame.explain()
print(plan)
```
-**Characteristics:**
-- 📊 Shows optimized query plan
-- 🔍 Understand Polars optimizations
-- 📈 Identify performance bottlenecks
-- 🎯 No actual execution
+!!! warning "Partial plans"
+ Explain uses the Polars plan directly. When part of the flow cannot be expressed as Polars, the plan shows only the convertible portion.
## System Architecture
-### Service Architecture
-
-```mermaid
-graph LR
- subgraph "Frontend"
- A[Designer Vue/Electron]
- end
-
- subgraph "Backend"
- B[Core Service FastAPI]
- C[Worker Service FastAPI]
- end
-
- subgraph "Storage"
- D[Arrow IPC Cache]
- end
-
- A <-->|Settings/Schema| B
- B <-->|Execution| C
- C <-->|Data| D
-```
-
-!!! info "Service Responsibilities"
- **Designer:**
- - Visual graph building interface
- - Node configuration forms (manually implemented)
- - Real-time schema feedback
-
- **Core:**
- - DAG management
- - Execution orchestration
- - Schema prediction
-
- **Worker:**
- - Polars transformations
- - Data caching (Arrow IPC)
- - Isolated from Core for scalability
+The three-service split (Designer, Core, Worker) and Arrow IPC data exchange are covered on the [Technical Architecture page](architecture.md#three-service-architecture).
### Project Structure
+Each Python package nests its importable code one level down (e.g. `flowfile_core/flowfile_core/`). The key directories:
+
```
-flowfile/
-├── flowfile_core/
-│ ├── nodes/ # Node implementations
-│ ├── schemas/ # Pydantic models
-│ └── flowfile/ # Graph management
-├── flowfile_worker/
-│ ├── execution/ # Polars execution
-│ └── cache/ # Arrow IPC caching
+Flowfile/
+├── flowfile_core/flowfile_core/
+│ ├── flowfile/ # FlowGraph, FlowNode, FlowDataEngine, node_designer
+│ ├── schemas/ # Pydantic settings + request/response models
+│ ├── configs/ # node_store/nodes.py registry, settings.py
+│ ├── routes/ # FastAPI routers
+│ ├── ai/ · kernel/ · catalog/ · auth/ · scheduler/ · alembic/
+├── flowfile_worker/flowfile_worker/ # flat modules: funcs.py, routes.py,
+│ # process_manager.py, viz_session_worker.py, …
+├── flowfile_frame/ # Polars-like Python API (FlowFrame, Expr)
└── flowfile_frontend/
- ├── components/ # Vue components
- └── electron/ # Desktop app
+ ├── src/renderer/ # Vue 3 renderer (shared by desktop + web)
+ └── src-tauri/ # Tauri 2 Rust shell + sidecar boot
```
## Contributing
-!!! warning "Current State of Node Development"
- While the backend architecture elegantly uses settings-driven nodes, adding new nodes requires work across multiple layers. The frontend currently requires manual implementation for each node type—the visual editor doesn't automatically generate forms from Pydantic schemas yet.
-
- However, there are also opportunities for more focused contributions! Integration with databases and cloud services is needed—these are smaller, more targeted tasks since the core structure is already in place. There's a lot of active development happening, so it's an exciting time to contribute!
-
-### Adding a New Node: The Full Picture
-
-Adding a node isn't as simple as defining settings and a function. Here's what's actually required:
+Adding a **built-in native node** touches multiple layers: the frontend needs a hand-written settings form (native nodes are not auto-generated from Pydantic schemas). Smaller, self-contained tasks — new database and cloud connectors — are a good entry point, because the surrounding structure already exists.
-#### Backend Requirements
+!!! tip "Prefer a custom node?"
+ If you want a new transformation without touching the frontend, build a [custom node](creating-custom-nodes.md) with the Node Designer API. Custom nodes get an auto-generated settings panel and land in the **User Defined Operations** palette section.
-1. **Define the Pydantic settings model** in `schemas/`
-2. **Implement the transformation method** on `FlowDataEngine`
-3. **Add the node method** to `FlowGraph` (e.g., `add_custom_transform()`)
-4. **Create the closure function** that captures settings
-5. **Define schema callbacks** for predicting output schemas
-6. **Register the node** in the node registry
-
-Example of what's really needed in FlowGraph:
-
-```python
-def add_custom_transform(self, transform_settings: input_schema.NodeCustomTransform):
- # Create the closure that captures settings
- def _func(fl: FlowDataEngine) -> FlowDataEngine:
- return fl.do_custom_transform(transform_settings.transform_input)
-
- # Register with the graph
- self.add_node_step(
- node_id=transform_settings.node_id,
- function=_func,
- node_type='custom_transform',
- setting_input=transform_settings,
- input_node_ids=[transform_settings.depending_on_id]
- )
-
- # Don't forget schema prediction!
- node = self.get_node(transform_settings.node_id)
- # ... schema callback setup ...
-```
+### Adding a native node: the full picture
-#### Frontend Requirements
+A native node is more than a settings model and a function.
-Currently, you'll need to:
+#### Backend
-1. Create a new Vue component for the node's configuration form
-2. Handle the visual representation in the graph editor
-3. Map the UI inputs to the backend settings structure
-4. Add the node type to the visual editor's palette
+1. Define the Pydantic settings model in `schemas/`.
+2. Implement the transformation method on `FlowDataEngine`.
+3. Add the node method to `FlowGraph` (e.g. `add_()`).
+4. Create the closure function that captures settings.
+5. Define a schema callback for predicting output schemas.
+6. Register the node in `configs/node_store/nodes.py`.
-This manual process ensures full control over the UI/UX but requires significant development effort.
+The `FlowGraph.add_` method follows the same closure pattern shown earlier — construct a `_func` that captures the settings and hand it to `add_node_step`. See `add_group_by` / `add_union` above for real examples.
-### Future Vision
+#### Frontend
-The goal is to eventually auto-generate UI from Pydantic schemas, which would complete the settings-driven architecture. This would make adding new nodes closer to just defining the backend settings and transformation logic, with the UI automatically following.
+1. Create a Vue settings component for the node's form.
+2. Handle its visual representation in the graph editor.
+3. Map the UI inputs to the backend settings structure.
+4. Add the node type to the palette.
-The beauty of Flowfile's architecture—discovered through the organic evolution from a UI-first approach—is that even though adding nodes requires work across multiple layers today, the settings-based design provides a clear contract between visual and code interfaces.
+!!! note "Future direction"
+ A long-term goal is to auto-generate the native-node settings UI from Pydantic schemas, the way custom nodes already work — which would reduce a native node to its backend settings and transformation logic. This is aspirational, not shipped.
-I hope you enjoyed learning about Flowfile's architecture and found the dual-interface approach as exciting as I do! If you have questions, ideas, or want to contribute, ]
-feel free to reach out via [GitHub](https://github.com/edwardvaneechoud/Flowfile) or check our [Core Developer Guide](flowfile-core.md). Happy building!
\ No newline at end of file
+Questions and ideas are welcome via [GitHub](https://github.com/edwardvaneechoud/Flowfile), and the [Core Developer Guide](flowfile-core.md) goes deeper on the internals.
\ No newline at end of file
diff --git a/docs/for-developers/docker-deployment.md b/docs/for-developers/docker-deployment.md
deleted file mode 100644
index b94f63111..000000000
--- a/docs/for-developers/docker-deployment.md
+++ /dev/null
@@ -1,220 +0,0 @@
-# Docker Deployment Guide
-
-Deploy Flowfile using Docker Compose for development and production environments.
-
-## Prerequisites
-
-- Docker and Docker Compose installed
-- Basic understanding of Docker concepts
-
-## Quick Start
-
-### Option A: Interactive Setup (Recommended)
-
-1. **Start the services:**
- ```bash
- docker compose up -d
- ```
-
-2. **Open Flowfile:** http://localhost:8080
-
-3. **Follow the Setup Wizard:**
- - Click "Generate Master Key"
- - Copy the generated key
- - Add to your `.env` file: `FLOWFILE_MASTER_KEY=`
- - Restart: `docker compose restart`
-
-4. **Log in** with default credentials (`admin` / `changeme`)
-
-### Option B: Pre-configured Setup
-
-For automated deployments:
-
-#### Step 1: Generate the Master Key
-
-```bash
-python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
-```
-
-Copy the output for the next step.
-
-#### Step 2: Configure Environment
-
-```bash
-cp .env.example .env
-```
-
-Edit `.env`:
-
-```bash
-FLOWFILE_MASTER_KEY=
-FLOWFILE_ADMIN_USER=admin
-FLOWFILE_ADMIN_PASSWORD=YourSecurePassword123!
-JWT_SECRET_KEY=your-secure-jwt-secret-at-least-32-chars
-```
-
-> **Git project tracking** is admin-only in docker and is gated by `FLOWFILE_ENABLE_PROJECTS`
-> (the `/project` router 404s when off). The bundled compose files default it to `true`; set
-> `FLOWFILE_ENABLE_PROJECTS=false` to disable it. Each user's projects are confined to their own
-> `/app/user_data/projects/` subtree, so tenants stay isolated.
-
-#### Step 3: Start Services
-
-```bash
-docker compose up -d
-```
-
-Access at: http://localhost:8080
-
-## Security Architecture
-
-| Component | Purpose | Configuration |
-|-----------|---------|---------------|
-| **Master Key** | Encrypts user secrets at rest | `FLOWFILE_MASTER_KEY` env var |
-| **User Secrets** | API keys, passwords, tokens | Encrypted in database |
-| **JWT Secret** | Signs authentication tokens | `JWT_SECRET_KEY` env var |
-| **User Password** | Authenticates users | Hashed in database |
-
-### How They Work Together
-
-1. User logs in with username/password
-2. Server issues a JWT token (signed with `JWT_SECRET_KEY`)
-3. User creates secrets (e.g., "my_api_key" = "sk-xxx")
-4. Secret value is encrypted using the master key before storage
-5. At runtime, secrets are decrypted with the master key for use in flows
-
-## Production Checklist
-
-- [ ] Generate a unique `FLOWFILE_MASTER_KEY`
-- [ ] Set a strong `FLOWFILE_ADMIN_PASSWORD`
-- [ ] Generate secure `JWT_SECRET_KEY` with `openssl rand -hex 32`
-- [ ] Never commit `.env` to version control
-- [ ] Back up `.env` securely (losing master key = losing all encrypted secrets)
-- [ ] Set up HTTPS (reverse proxy with nginx/traefik)
-- [ ] Configure firewall rules
-
-## Docker Compose Services
-
-```yaml
-services:
- flowfile-frontend: # Web UI on port 8080
- flowfile-core: # API server on port 63578
- flowfile-worker: # Background job processor on port 63579
-```
-
-All services share:
-- The master key via `FLOWFILE_MASTER_KEY` environment variable
-- User data volume (`./flowfile_data`)
-- Internal network for communication
-
-## Troubleshooting
-
-### Setup wizard keeps appearing
-
-The master key is not configured. Add `FLOWFILE_MASTER_KEY` to your `.env` file and restart.
-
-### Invalid master key format
-
-The master key must be a valid Fernet key. Generate a new one:
-
-```bash
-python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
-```
-
-**Warning:** Changing the key makes existing encrypted secrets unreadable.
-
-### Container fails to start
-
-Check the logs:
-
-```bash
-docker compose logs flowfile-core
-docker compose logs flowfile-worker
-```
-
-## Volumes
-
-| Volume | Purpose |
-|--------|---------|
-| `./flowfile_data` | User uploads, files |
-| `./saved_flows` | Saved flow definitions |
-| `flowfile-internal-storage` | Internal application data |
-
-## Scaling Considerations
-
-For high-availability deployments:
-- Use external PostgreSQL instead of SQLite
-- Deploy multiple worker instances
-- Use Redis for distributed task queue
-- Place services behind a load balancer
-
-## Group-Based Sharing (multi-user mode)
-
-In Docker (multi-user) mode, Flowfile supports sharing resources with **user groups**.
-This feature is dormant in the desktop/Electron app.
-
-### ⚠️ Breaking change: the catalog is now private-by-default
-
-Previously the data catalog (namespaces, tables, flows, runs, visualizations,
-dashboards) was **globally visible** to every authenticated user in Docker mode.
-As of the release that introduces group sharing, the catalog is
-**private-by-default**: each user sees only the resources they own, the ones
-explicitly shared with a group they belong to, and the seeded *public* system
-namespaces (`General`, `default`, `Unnamed Flows`, `Local Flows`) as tree
-containers. Global admins still see everything.
-
-If your team relied on the old "everyone sees every flow/table" behavior, create a
-group, add the relevant members, and share the namespaces/tables/flows you want
-shared. No data is lost in the upgrade — only its *visibility* narrows.
-
-### How it works
-
-- **Groups**: only global admins create groups (`/user-groups`, or the **User
- Groups** sidebar entry). Each group has member roles (`owner` / `manager` /
- `member`) so a group owner can manage membership without being a global admin.
-- **Sharing**: owners share a resource with a group at `use` (read/execute) or
- `manage` (edit + re-share) level via the **Share** action in the UI.
- - *Secrets* are **use-only** when shared: a group's flows can run with the
- credential, but members can never view its value. Sharing a secret for use is
- therefore security-equivalent to giving the group that credential.
- - *Connections* (database, cloud, Google Analytics, Kafka): a shared connection
- is usable in flows directly. A `manage`-grantee who changes the connection's
- target (host/endpoint/protocol) must re-enter the credentials.
-- **Schedules**: a `use`-level member may schedule a shared flow; the run executes
- as that member, so its secret/connection references resolve against the member's
- own grants. Make sure the member also has access to everything the flow uses.
-- **Not shared**: AI BYOK keys and uploaded files stay outside the sharing model
- (the Docker uploads directory is already common to all users).
-
-### How shared secrets decrypt for non-owners
-
-Sharing is **authorization-only**: granting a group access to a secret or
-connection never copies, re-encrypts, or exposes the credential.
-
-Every secret is stored as `$ffsec$1$$`, encrypted with a
-per-user key derived from the master key (HKDF with the owner's user id as
-context). Because the owner's id is embedded in the stored value, decryption
-never depends on *who is running the flow*: when a group member's flow resolves
-a shared secret, core (and the worker, which derives keys independently) reads
-the owner id out of the value and re-derives the **owner's** key. The member's
-identity only matters for the authorization check — may this user resolve this
-secret by name — which is grant-based: own secrets first, then group-shared ones.
-
-Consequences:
-
-- The plaintext is never returned to non-owners through the API; it only
- resolves inside flow execution.
-- When a `manage`-grantee rotates a shared connection's credential, the new
- value is re-encrypted under the **owner's** key, so the stored format never
- changes hands.
-- Revoking a grant takes effect immediately: the grantee never held key
- material, so there is nothing to rotate or re-encrypt.
-
-### Operator notes
-
-- `FLOWFILE_INTERNAL_SERVICE_USER_ID` (default `1`) selects which user the kernel
- internal-service principal maps to when no kernel id is supplied. In hardened
- deployments point it at a dedicated service account that owns no resources.
-- Migration `020` adds the `user_groups`, `user_group_memberships`, and
- `resource_grants` tables and an `is_public` flag on namespaces; it runs
- automatically at core startup.
diff --git a/docs/for-developers/flowfile-core.md b/docs/for-developers/flowfile-core.md
index 04b852e15..2e11a9ac8 100644
--- a/docs/for-developers/flowfile-core.md
+++ b/docs/for-developers/flowfile-core.md
@@ -1,16 +1,11 @@
# Flowfile Core: A Developer's Guide
-Welcome! This guide is for developers who want to understand, use, and contribute to `flowfile-core`. We'll dive into the architecture, see how data flows, and learn how to build powerful data pipelines.
+This guide is for developers who want to understand and contribute to `flowfile-core`. It walks the architecture from the outside in — building a small pipeline in Python step by step, then explaining how the graph decides where each node runs.
-!!! info "Looking for the API docs?"
- - **[Python API Reference for users](../users/python-api/index.md)**: If you want to USE Flowfile
- - **[Design Philosophy](design-philosophy.md)**: If you want to understand WHY Flowfile works this way
- - **This page**: If you want to understand HOW Flowfile works internally
-
-!!! tip "New to Flowfile?"
- If you're looking for the high-level Python API, start with the [Python API Overview](../users/python-api/index.md). This guide dives into the internal architecture.
-
-Ready? Let's build something!
+!!! info "Looking for something else?"
+ - **[Python API Reference](../users/python-api/index.md)**: to USE Flowfile
+ - **[Design Philosophy](design-philosophy.md)**: to understand WHY Flowfile works this way
+ - **This page**: to understand HOW Flowfile works internally
---
@@ -21,7 +16,7 @@ At its heart, `flowfile-core` is composed of three main objects:
1. **`FlowGraph`**: The central orchestrator. It holds your pipeline, manages the nodes, and controls the execution flow.
2. **`FlowNode`**: An individual step in your pipeline. It's a wrapper around your settings and logic, making it an executable part of the graph.
-3. **`FlowDataEngine`**: The data itself, which flows between nodes. It's a smart wrapper around a [Polars LazyFrame](https://pola-rs.github.io/polars/py-polars/html/reference/lazyframe/index.html), carrying both the data and its schema.
+3. **`FlowDataEngine`**: The data itself, which flows between nodes. It's a wrapper around a [Polars LazyFrame](https://pola-rs.github.io/polars/py-polars/html/reference/lazyframe/index.html), carrying both the data and its schema.
Let's see these in action.
@@ -90,9 +85,7 @@ It runs successfully but does nothing, as expected. The FlowGraph's job is to:
Let's give it a node to manage.
## 2. Adding a Node: Where Settings Come to Life
-You don't add raw functions or data directly to the graph. Instead, you provide **settings objects** (which are just Pydantic models). The graph then transforms these settings into executable `FlowNodes`.
-
-Watch this:
+You don't add raw functions or data directly to the graph. Instead, you provide **settings objects** (Pydantic models). The graph turns these settings into executable `FlowNodes`.
```python
from flowfile_core.schemas import input_schema
@@ -178,7 +171,7 @@ print(f"Nodes executed: {result.nodes_completed}/{len(graph.nodes)}")
# Nodes executed: 1/2
```
-Only one node ran! Why? The graph is smart; it knows the filter node has no input, thus will never succeed running.
+Only one node ran. The graph knows the filter node has no input, so it can never run successfully and is skipped.
Why the filter node was skipped
@@ -244,10 +237,10 @@ for node_result in result.node_step_result:
# - Node 2 ran successfully: True
```
-Success! Both nodes executed. The connection allowed data to flow from the input to the filter.
+Both nodes executed. The connection let data flow from the input to the filter.
## 4. The FlowDataEngine: The Data Carrier
-When data moves from one node to another, it's bundled up in a FlowDataEngine object. This isn't just raw data; it's an enhanced wrapper around a Polars LazyFrame.
+When data moves from one node to another, it's bundled up in a FlowDataEngine object. This isn't just raw data; it's a wrapper around a Polars LazyFrame.
```python
# Let's inspect the data after the run
@@ -276,7 +269,7 @@ The `FlowDataEngine` is the boundary between `flowfile-core` and Polars. It:
* Tracks metadata like record counts.
* Manages lazy vs. eager execution.
-## 5. The Hash System: Smart Change Detection
+## 5. The Hash System: Change Detection
How does the graph know when to re-run a node? Every `FlowNode` has a unique hash based on its configuration and its inputs.
```python
@@ -310,10 +303,10 @@ This hash is calculated from:
This creates a chain of **dependency**. If you change a node, `flowfile-core` knows that it and all downstream nodes need to be re-run, while upstream nodes can use their cached results. This is crucial for efficiency.
-## 6. Schema Prediction: See the Future
-One of the most powerful features for interactive UI is **schema prediction**. A node can predict its output schema _without_ processing any data.
+## 6. Schema Prediction
+**Schema prediction** is what makes the interactive UI responsive: a node computes its output schema _without_ processing any data.
-Let's add a "formula" node to create a new column.
+Add a "formula" node to create a new column.
```python
from flowfile_core.schemas.transform_schema import FunctionInput, FieldInput
@@ -351,7 +344,6 @@ print(f"\nHas the formula node run? {formula_node.node_stats.has_run_with_curren
Predicted columns for Node 3:
- name (Type: String)
- age (Type: Int64)
- - city (Type: String)
- age_doubled (Type: Int64)
```
@@ -379,7 +371,7 @@ Let's recap the entire lifecycle:
* **Results Flow Through:** The data, wrapped in the `FlowDataEngine`, moves down the pipeline, getting transformed at each step.
-This architecture provides a powerful combination of flexibility, introspection, and performance, bridging the gap between a visual, no-code interface and a powerful, code-driven engine.
+This architecture combines flexibility, introspection, and performance, bridging a visual no-code interface and a code-driven engine that share one graph.
@@ -396,7 +388,7 @@ The behavior depends on two settings visible in the frontend:
### Execution Modes from the Frontend
-In the UI, users choose between **Development** and **Performance** mode. Here's what each mode does when running remotely:
+Users choose between **Development** and **Performance** mode in the UI. The [Technical Architecture page](architecture.md#execution-modes) is the canonical description of what each mode does; the section here focuses on how the mode maps to an execution *strategy* per node when running remotely.
#### Development Mode (remote)
@@ -404,9 +396,9 @@ Development mode is the interactive, debugging-friendly mode. It produces **prev
| Node type | What happens |
|---|---|
-| **Narrow transforms** (select, sample, union) | Computation runs locally, only a 100-row sample is sent to the remote worker for preview (`LOCAL_WITH_SAMPLING`) |
-| **Wide transforms** (sort, record_count) | Entire computation runs on the remote worker (`REMOTE`) |
-| **Everything else** (filter, join, group_by, input nodes) | Entire computation runs on the remote worker (`REMOTE`) |
+| **Narrow transforms** (select, sample, union, filter) | Computation runs locally, only a 100-row sample is sent to the remote worker for preview (`LOCAL_WITH_SAMPLING`) |
+| **Wide transforms** (sort, record_count, join, group_by) | Entire computation runs on the remote worker (`REMOTE`) |
+| **Other nodes** (input nodes, manual_input) | Entire computation runs on the remote worker (`REMOTE`) |
| **Any node with `cache_results` enabled** | Always fully remote regardless of transform type (`REMOTE`) |
#### Performance Mode (remote)
@@ -502,26 +494,22 @@ The strategy routing depends on how nodes are classified in the node store:
| select | narrow | LOCAL_WITH_SAMPLING | skip if already ran |
| sample | narrow | LOCAL_WITH_SAMPLING | skip if already ran |
| union | narrow | LOCAL_WITH_SAMPLING | skip if already ran |
+| filter | narrow | LOCAL_WITH_SAMPLING | skip if already ran |
| sort | wide | REMOTE | skip if already ran |
| record_count | wide | REMOTE | skip if already ran |
-| filter | other | REMOTE | skip if already ran |
-| join | other | REMOTE | skip if already ran |
-| group_by | other | REMOTE | skip if already ran |
+| join | wide | REMOTE | skip if already ran |
+| group_by | wide | REMOTE | skip if already ran |
| manual_input | other | REMOTE | skip if already ran |
-Narrow transforms are defined with `transform_type="narrow"` in `configs/node_store/nodes.py`.
+Each node's `transform_type` (`narrow` / `wide` / `other`) is set in `configs/node_store/nodes.py`. Only `narrow` transforms take the `LOCAL_WITH_SAMPLING` path; `wide` and `other` go `REMOTE`.
---
## The FastAPI Service: Your API Layer
-While `FlowGraph`, `FlowNode`, and `FlowDataEngine` power the core pipeline logic, the **FastAPI service** is what makes it accessible from the outside world.
+While `FlowGraph`, `FlowNode`, and `FlowDataEngine` power the core pipeline logic, the **FastAPI service** exposes it over HTTP:
-Think of it as the **control panel** for your pipelines:
-
-- **HTTP interface** – Wraps the core Python objects in a REST API so UIs (like Flowfile’s) or other systems can create, run, and inspect flows via standard web requests.
-- **State management** – Keeps track of all active `FlowGraph` sessions. When the UI triggers a change, it’s really calling one of these endpoints, which updates the in-memory graph.
+- **HTTP interface** – Wraps the core Python objects in a REST API so UIs (like Flowfile's) or other systems can create, run, and inspect flows via standard web requests.
+- **State management** – Keeps track of all active `FlowGraph` sessions. When the UI triggers a change, it's really calling one of these endpoints, which updates the in-memory graph.
- **Security** – Handles authentication and authorization so only the right users can access or modify flows.
-- **Data previews** – When you view a node’s output in the UI, the API calls `.get_resulting_data()` on the corresponding `FlowNode` and returns a sample to the client.
-
-In short: **FastAPI turns the in-memory power of `flowfile-core` into a secure, interactive web service**, enabling rich, real-time applications to be built on top of your pipelines.
+- **Data previews** – When you view a node's output in the UI, the API calls `.get_resulting_data()` on the corresponding `FlowNode` and returns a sample to the client.
diff --git a/docs/for-developers/index.md b/docs/for-developers/index.md
index b4d92535a..76cd4d375 100644
--- a/docs/for-developers/index.md
+++ b/docs/for-developers/index.md
@@ -1,6 +1,6 @@
-# **Flowfile: For Developers**
+# Flowfile for developers
-Welcome to the developer documentation for Flowfile. This is the home for anyone who wants to contribute to the platform or understand its internal architecture.
+This section is for anyone contributing to Flowfile or working out how its internals fit together. After reading it you will know how the visual editor and the Python API construct the same objects, how the three services divide the work, and where to change code when you add a node, a kernel feature, or an AI surface.
!!! note "Looking to use the Python API?"
If you want to **use** Flowfile's Python API to build data pipelines, check out the [Python API User Guide](../users/python-api/index.md). This developer section focuses on Flowfile's internal architecture and design philosophy.
@@ -10,13 +10,7 @@ Welcome to the developer documentation for Flowfile. This is the home for anyone
Flowfile is built on an architecture where the Python API and the visual editor are two interfaces to the exact same underlying objects: the **`FlowGraph`** and its **`FlowNodes`**.
-When you write `df.filter(...)`, you programmatically construct a `FlowNode` and attach it to the `FlowGraph`. When a user drags a "Filter" node in the UI, they create the identical object. This **dual interface philosophy** means your work is never locked into one paradigm.
-
-- **`FlowGraph`**: The central orchestrator that holds the complete definition of your pipeline—every node, setting, and connection.
-- **`FlowNode`**: An individual, executable step in your pipeline that wraps settings and logic.
-- **`FlowDataEngine`**: A smart wrapper around a Polars `LazyFrame` that carries the data and its schema between nodes.
-
-Learn more about this in our **[Dual Interface Philosophy](design-philosophy.md)** guide.
+When you write `df.filter(...)`, you programmatically construct a `FlowNode` and attach it to the `FlowGraph`. When a user drags a "Filter" node in the UI, they create the identical object. The `FlowGraph` orchestrates the pipeline, each `FlowNode` wraps a step's settings and logic, and a `FlowDataEngine` carries the data and schema between nodes — see the [Dual Interface Philosophy](design-philosophy.md) guide.
---
## Getting Started with Development
@@ -27,42 +21,24 @@ To contribute to Flowfile, you should be familiar with:
- **Required Knowledge**: Python 3.10+, and a basic familiarity with Polars or Pandas.
- **Helpful Knowledge**: Experience with Polars LazyFrames, Directed Acyclic Graphs (DAGs), and Pydantic.
-### 2. Set Up Your Environment
+### 2. Set up your environment
-Before diving in, clone the repository and install the dependencies using Poetry:
+Clone the repository and install the Python dependencies with Poetry:
```bash
-# For development/contributing
-git clone [https://github.com/edwardvaneechoud/Flowfile](https://github.com/edwardvaneechoud/Flowfile)
+git clone https://github.com/edwardvaneechoud/Flowfile
cd Flowfile
poetry install
```
-### 3. See It in Action: A Quick Example
-The following code builds a data pipeline using the Python API. This same pipeline can be generated visually in the UI.
+`poetry install` is enough to run `flowfile_core`, use the Python API, and run the test suite. Building the desktop app additionally needs the PyInstaller group (`poetry install --with build`), and the visual editor needs the frontend toolchain (`cd flowfile_frontend && npm install`). See [CONTRIBUTING.md](https://github.com/edwardvaneechoud/Flowfile/blob/main/CONTRIBUTING.md) and the root `Makefile` for the full build.
+
+### 3. See it in action
+
+The following pipeline uses the Python API. Building the same steps visually produces the identical `FlowGraph`.
```python
-import flowfile as ff
-from flowfile import col
-
-# Create a FlowFrame from a local CSV
-df = ff.read_csv("sales_data.csv", description="Load raw sales data")
-
-# Build a transformation pipeline with a familiar, chainable API
-processed_sales = (
- df.filter(col("amount") > 100, description="Filter for significant sales")
- .with_columns(
- (col("quantity") * col("price")).alias("total_revenue")
- )
- .group_by("region", description="Aggregate sales by region")
- .agg(
- col("total_revenue").sum()
- )
-)
-
-# Get your results as a Polars DataFrame
-results_df = processed_sales.collect()
-print(results_df)
+--8<-- "docs/examples/sales_pipeline.py:example"
```
## Documentation Guides
@@ -74,10 +50,12 @@ print(results_df)
---
## Contributing to Flowfile
-We welcome contributions! Adding a new node requires changes across the stack:
+Adding a built-in native node touches the whole stack:
+
+- **Backend**: define Pydantic settings models, implement the transformation logic on `FlowDataEngine`, and register the node on `FlowGraph`.
+- **Frontend**: hand-write a Vue component for the node's settings form. Native nodes do not get an auto-generated form.
-- **Backend**: You'll need to define Pydantic setting models, implement the transformation logic in the `FlowDataEngine`, and register the new node in the `FlowGraph`.
-- **Frontend**: Currently, you must also manually create a Vue component for the node's configuration form in the visual editor.
+Custom (user-defined) nodes are the exception: they get a schema-driven settings panel generated from the [Node Designer](../users/visual-editor/node-designer.md) API, so no Vue is needed. See [Creating Custom Nodes](creating-custom-nodes.md).
-For a more detailed breakdown, please read the **[Contributing section in our Design Philosophy guide](design-philosophy.md#contributing)**.
+For the full walkthrough, read the [Contributing section of the Design Philosophy guide](design-philosophy.md#contributing).
diff --git a/docs/for-developers/kernel-architecture.md b/docs/for-developers/kernel-architecture.md
index d0a5f610c..e9f22da6b 100644
--- a/docs/for-developers/kernel-architecture.md
+++ b/docs/for-developers/kernel-architecture.md
@@ -3,7 +3,9 @@
The kernel system provides isolated Python code execution inside Docker containers. This page explains the internal architecture, component interactions, and key design decisions.
!!! info "Looking for the user guide?"
- See [Kernel Execution](../users/visual-editor/kernels.md) for the user-facing documentation on how to write code and use the `flowfile` API inside kernels.
+ See [Kernel Execution](../users/visual-editor/kernels.md) for the user-facing documentation on how to write code and use the `flowfile_ctx` API inside kernels.
+
+Kernels are one half of a pair: the catalog is where their inputs and outputs live — Delta write-back, global artifacts, and the notebooks whose Python cells execute here. [Catalog Architecture](catalog-architecture.md) (especially its [Notebooks](catalog-architecture.md#notebooks) section) covers that side of the contract.
---
@@ -16,7 +18,7 @@ The kernel system consists of two main components:
```mermaid
graph LR
- Frontend["Frontend (Vue/Electron)"]
+ Frontend["Frontend (Vue/Tauri)"]
Core["Core API (port 63578)"]
Manager["Kernel Manager (Docker API)"]
K1["Kernel Container (port 9999)"]
@@ -49,7 +51,7 @@ The `KernelManager` is a singleton that runs inside the Core service. It manages
| Operation | What Happens |
|-----------|-------------|
| **Create** | Allocates a `KernelInfo` record, persists config to the database |
-| **Start** | Verifies the `flowfile-kernel` Docker image exists, runs `docker.containers.run()`, polls `/health` until ready (120s timeout) |
+| **Start** | Verifies the pinned kernel image (`flowfile-kernel-{base,ml,lite}:`) exists, runs `docker.containers.run()`, polls `/health` until ready (120s timeout) |
| **Execute** | Serializes inputs to parquet, sends `ExecuteRequest` via HTTP, tracks kernel state |
| **Stop** | Stops and removes the Docker container |
| **Delete** | Stops if running, removes from in-memory registry and database |
@@ -92,13 +94,13 @@ Flowfile does **not** define compatibility *ranges* between app and kernel versi
### Three independent version numbers
-| Version | Where | Example | Role |
-|---------|-------|---------|------|
-| App / root | root `pyproject.toml` | `0.11.0` | The Flowfile release |
-| Kernel **image** tag | `flowfile_core/flowfile_core/kernel/manager.py` (`_KERNEL_IMAGE_{BASE,ML,LITE}_DEFAULT`) | `0.4.0` | The image the app pulls / runs |
-| Kernel **runtime API** | `kernel_runtime/__init__.py` (`__version__`) | `0.3.0` | The kernel's HTTP API version, reported by `/health` |
+| Version | Where | Role |
+|---------|-------|------|
+| App / root | root `pyproject.toml` `version` | The Flowfile release |
+| Kernel **image** tag | `flowfile_core/flowfile_core/kernel/manager.py` (`_KERNEL_IMAGE_{BASE,ML,LITE}_DEFAULT`, `0.4.0` as of 2026-07) | The image the app pulls / runs |
+| Kernel **runtime API** | `kernel_runtime/__init__.py` (`__version__`) | The kernel's HTTP API version, reported by `/health` |
-These evolve **independently** — bumping the app does not require bumping the kernel image, and vice versa.
+Read each value from its source rather than assuming a number — the three are decoupled. They evolve **independently**: bumping the app does not require bumping the kernel image, and vice versa.
### How the pin works
@@ -106,7 +108,7 @@ These evolve **independently** — bumping the app does not require bumping the
- Core reads the running kernel's runtime version from `/health` into `KernelInfo.kernel_version` **for display only** (the "Kernel runtime" line in the Kernel Manager). There is no min/max gate and nothing that rejects or warns about an "out-of-range" kernel.
- The only **hard** coupling is **polars**: `kernel_runtime` pins a polars (and the `polars-ds` plugin) compatible with the app's `polars >=1.8.2,<1.40`. These must be bumped together, but that compatibility is guaranteed at *image-build time* via the pinned tag — not by a runtime check.
-So the practical contract is "use the pinned tag." An older kernel image is **not blocked** — it simply may lack fixes or features the app expects (for example, JSON-format global-artifact deserialization was only fixed in image `0.3.1`). That gap is surfaced as a non-blocking **"Update available"** hint in the Kernel Manager (per flavour) and the kernel details modal (per kernel), rather than a hard version gate.
+So the practical contract is "use the pinned tag." An older kernel image is **not blocked** — it simply may lack fixes or features the app expects. That gap is surfaced as a non-blocking **"Update available"** hint in the Kernel Manager (per flavour) and the kernel details modal (per kernel), rather than a hard version gate.
### Shipping a kernel change
@@ -265,13 +267,14 @@ All endpoints require JWT authentication and enforce user ownership:
| `DELETE /kernels/{id}` | Delete kernel |
| `POST /kernels/{id}/execute` | Execute code |
| `POST /kernels/{id}/execute_cell` | Execute in interactive mode |
-| `POST /kernels/{id}/interrupt` | Cancel execution |
| `GET /kernels/{id}/artifacts` | List artifacts |
| `POST /kernels/{id}/clear` | Clear all artifacts |
| `GET /kernels/{id}/display_outputs` | Get display outputs |
| `GET /kernels/{id}/memory` | Get memory usage |
| `GET /kernels/docker-status` | Check Docker availability |
+There is no core-side interrupt route. Cancellation is a kernel-runtime concern: the runtime container exposes `POST /interrupt` on its own port (see [Thread-based Execution](#thread-based-execution)), which Core reaches directly, not through a `/kernels/{id}/interrupt` proxy.
+
### Database Persistence
**Location:** `flowfile_core/flowfile_core/kernel/persistence.py`
@@ -325,14 +328,18 @@ Artifact serialization uses pickle/cloudpickle. This is acceptable because:
### Building the Kernel Image
+The resolver looks for `flowfile-kernel-{base,ml,lite}:local` when the pinned registry tag isn't present locally, so tag your local build to match the flavour you want to run:
+
```bash
-# Via docker compose
-docker compose build flowfile-kernel
+# Via docker compose (builds the base flavour as flowfile-kernel-base:local)
+docker compose --profile kernel build flowfile-kernel
-# Or directly
-docker build -t flowfile-kernel -f kernel_runtime/Dockerfile kernel_runtime/
+# Or directly — tag the flavour explicitly
+docker build -t flowfile-kernel-base:local -f kernel_runtime/Dockerfile kernel_runtime/
```
+A bare `docker build -t flowfile-kernel …` produces a tag the app will not pick up.
+
The image is based on `python:3.12-slim` and includes:
- **Data stack:** Polars, PyArrow, NumPy
@@ -342,21 +349,21 @@ The image is based on `python:3.12-slim` and includes:
### Docker Compose Integration
-The `docker-compose.yml` includes a build-only service for the kernel image:
+The `docker-compose.yml` includes a build-only service per kernel flavour (`flowfile-kernel`, `flowfile-kernel-ml`, `flowfile-kernel-lite`). The base flavour:
```yaml
flowfile-kernel:
build:
context: kernel_runtime
dockerfile: Dockerfile
- image: flowfile-kernel
+ image: flowfile-kernel-base:local
entrypoint: ["true"]
restart: "no"
profiles:
- kernel
```
-This service is not started by `docker compose up` — it only builds the image. The Core service creates kernel containers dynamically via the Docker API.
+These services are not started by `docker compose up` — the `kernel` profile only builds the images (the `entrypoint: ["true"]` exits immediately). The Core service creates kernel containers dynamically via the Docker API, using the `flowfile-kernel-{base,ml,lite}:local` tags.
!!! warning "Docker Socket"
The Core service requires access to the Docker socket (`/var/run/docker.sock`) to manage kernel containers. In production, consider using a Docker socket proxy (e.g., `tecnativa/docker-socket-proxy`) to restrict API access.
@@ -387,8 +394,8 @@ python -m pytest kernel_runtime/tests -v
### Integration Tests (Docker Required)
```bash
-# Build the kernel image first
-docker build -t flowfile-kernel -f kernel_runtime/Dockerfile kernel_runtime/
+# Build the base kernel image first (tag must match the resolver's flavour tag)
+docker build -t flowfile-kernel-base:local -f kernel_runtime/Dockerfile kernel_runtime/
# Run kernel integration tests
poetry run pytest flowfile_core/tests -m kernel -v
diff --git a/docs/for-developers/python-api-reference.md b/docs/for-developers/python-api-reference.md
index c171bf120..af4c06e35 100644
--- a/docs/for-developers/python-api-reference.md
+++ b/docs/for-developers/python-api-reference.md
@@ -53,10 +53,8 @@ The `FlowDataEngine` is the primary engine of the library, providing a rich API
unwrap_annotated: true
show_symbol_type_toc: true
-#### FlowfileColumn
-
-### `FlowfileColumn`
-The `FlowfileColumn` is a data class that holds the schema and rich metadata for a single column managed by the `FlowDataEngine`.
+### FlowfileColumn
+The `FlowfileColumn` holds the schema and metadata for a single column managed by the `FlowDataEngine`.
::: flowfile_core.flowfile.flow_data_engine.flow_file_column.main.FlowfileColumn
options:
diff --git a/docs/for-developers/visualizations.md b/docs/for-developers/visualizations.md
index eb19d1854..09a560325 100644
--- a/docs/for-developers/visualizations.md
+++ b/docs/for-developers/visualizations.md
@@ -40,7 +40,7 @@ spawned child imports Polars, holds the LazyFrame, runs polars-gw
Two boundaries do real work here:
-- **Core never touches a `LazyFrame`.** It resolves *where* a visualization reads from — a Delta table, a SQL string, a Python flow — and ships a small descriptor to the worker. That keeps core small and responsive.
+- **Core never touches a `LazyFrame`.** `VisualizationService` resolves *where* a visualization reads from — a Delta table, a SQL string, a Python flow — and ships a small descriptor to the worker. That keeps core small and responsive.
- **The worker's FastAPI process doesn't import Polars either.** It dispatches. The actual heavy lifting — Polars, polars-gw, dataset memory — lives in spawned child processes that the parent only knows about through queues.
The second boundary is the unusual one. Polars is memory-heavy and aggressively multithreaded; mixing it into the request-serving process means one slow query starves every other chart. Pushing it out to a child fixes that.
@@ -58,7 +58,7 @@ Two properties fall out of how the pool is keyed:
- **Same source, same child.** Successive queries from one visualization reuse the warm `LazyFrame`. A per-handle lock guarantees that two browser tabs editing the same visualization don't trip over each other's responses.
- **Different sources, different children, in parallel.** Two users on two tables — or one user with two visualization tabs open — run in separate processes that don't block each other. There is no global lock between sessions.
-`flowfile_worker/viz_sessions.py` is the registry. `flowfile_worker/viz_session_worker.py` is the entry point that runs inside each spawned process — and the **only** place anywhere in the worker that imports Polars and polars-gw.
+`flowfile_worker/flowfile_worker/viz_sessions.py` is the registry. `flowfile_worker/flowfile_worker/viz_session_worker.py` is the entry point that runs inside each spawned process — and the **only** place in the worker that imports **polars-gw** and holds the visualization dataset in memory. (Plain `polars` is imported by other worker modules such as `funcs.py` and `catalog_reader.py`; it's the polars-gw session data that is confined to the spawned child.)
---
@@ -74,13 +74,13 @@ Each child holds a real, live dataset, so left alone they accumulate and the wor
Every one of these paths runs the child through the same shutdown sequence: send a graceful stop, wait briefly, terminate, kill if it's still alive, drop references. Nothing is kept around on the assumption it might be needed later.
-The actual numeric thresholds live as constants at the top of `viz_sessions.py` and are tunable knobs, not load-bearing magic.
+The actual numeric thresholds live as constants near the top of `flowfile_worker/flowfile_worker/viz_sessions.py` (`IDLE_TTL_SECONDS`, `MAX_SESSIONS`, `REAP_INTERVAL_SECONDS`, `MAX_REQUESTS_PER_CHILD`, `MAX_CHILD_LIFETIME_SECONDS`) and are tunable knobs, not load-bearing magic.
---
## What a "visualization" looks like at rest
-A visualization is a row in `catalog_visualizations`: a name, a Graphic Walker chart spec (JSON, possibly multi-tab), a pointer to either a catalog table or an inline SQL query, and a thumbnail PNG captured client-side at save time. Schemas live in `flowfile_core/schemas/catalog_schema.py`.
+A visualization is a row in `catalog_visualizations`: a name, a Graphic Walker chart spec (JSON, possibly multi-tab), a pointer to either a catalog table or an inline SQL query, and a thumbnail PNG captured client-side at save time. Schemas live in `flowfile_core/flowfile_core/schemas/catalog_schema.py`.
A visualization never embeds the data — it embeds *how to find the data*. That's why deleting the underlying table doesn't cascade-delete the visualization (it becomes orphaned and the library labels it as such), why moving a visualization between namespaces is free, and why a SQL visualization can reference whatever tables exist in the catalog at query time.
@@ -91,7 +91,7 @@ A visualization never embeds the data — it embeds *how to find the data*. That
To support a new source — a remote Postgres, a parquet on S3, anything — the touchpoints are:
1. Extend the source descriptor in core, and the matching worker model, so the new kind is expressible.
-2. Teach `CatalogService` to translate it into a worker descriptor and emit a deterministic `session_key`. The session key is what lets the pool reuse children.
+2. Teach `VisualizationService` (`flowfile_core/flowfile_core/catalog/services/visualizations.py`) to translate it into a worker descriptor and emit a deterministic `session_key` (`_session_key_for_table` and the `session_key` fields). The session key is what lets the pool reuse children.
3. Teach `viz_session_worker._build_viz_loader_in_child` to open the new kind as a Polars `LazyFrame`.
Path validation and source resolution live in core on purpose. The worker child should treat its inputs as already-validated names plus an opaque chart payload — defence-in-depth is core's job.
diff --git a/docs/index.html b/docs/index.html
index 04dce162f..3d48f45fe 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -5,19 +5,19 @@
Flowfile - Visual ETL Tool
-
-
-
+
Flowfile
- An open-source data platform with a visual pipeline builder, AI assistant, data catalog, Delta Lake storage,
- scheduling, Kafka ingestion, sandboxed Python execution, and a Polars-compatible API — all in a single pip install.
+ An open-source data platform with a visual pipeline builder, AI assistant, data catalog with Delta Lake storage,
+ scheduling, Kafka ingestion, sandboxed Python execution, and a Polars-compatible API — installed with a single pip install.
+
- Use the visual designer for exploration or write code for automation - seamlessly switch between both
+ The same sales pipeline — cleaned, filtered, and aggregated — on the canvas and in Python.
+ Both produce the same flow; switch whenever you like.
+
+
-
-
📊 Visual Approach
-
-
Drag and drop nodes to build workflows
-
Configure transformations with intuitive forms
-
Preview data at each step instantly
-
Export your pipeline as Python code
-
Perfect for exploration and prototyping
-
+
On the canvas
+
+
Read — load supermarket_sales.csv
+
Drop Duplicates — remove repeated invoice rows
+
Filter — keep bulk orders: [quantity] > 7
+
Group By — per city, the sum and median of gross income
+ Also available in the app: Create → From template → "Sales pipeline". The flow and the code above are
+ validated by the test suite on every commit.
+
-
-
-
See It For Yourself
-
- Start rediscovering how we bridge the gap between business users and technical users.
-