Share code between workflows
Three workflows that all talk to the same system usually end up with the same helper file in all three — a tracker.py, a pricing calculation, a formatter. Copying it is easy on the day and expensive afterwards: the copies drift, nothing in Ronja knows they were ever the same file, and a fix has to be applied three times by whoever remembers.
A shared module is that helper kept once. It is a named Python package that lives in a feature, is versioned like everything else in Ronja, and is imported by workflows rather than copied into them.
A module has no entrypoint, no parameters and no runs — there is nothing to run. It is code your workflows call, so it never appears in a run list. If what you want is a job with its own run history, that is a workflow, and a workflow can call another workflow.
Make one
Section titled “Make one”ronja module — aliased mod — keeps a module’s .py files in a folder on your machine, the same way ronja wf keeps a workflow’s.
ronja module init emab_tracker --feature <feature ID> # set up this foldercp ../rfq-pipeline/tracker.py . # move the helper inronja module push # create it, and sync the folderronja module publish # take it live, or ask an admin toThe name you pass to init is the Python package your workflows will import, so it has to be a valid Python identifier — letters, digits and underscores, not starting with a digit — unique among the live modules in your organization, and not the name of something Python or the workflow runtime already provides (json, requests, pandas, tools, …). A name that shadows one of those is refused, because import json would go on resolving to the built-in one and your code would never run.
The feature ID appears in the feature’s URL in the app. Nothing exists in Ronja until your first push, which prints the module’s id — you need it for the marker below, and ronja module status prints it again any time.
| Command | What it does |
|---|---|
init <name> --feature <id> |
Sets up the folder you are in: the manifest, an __init__.py if there is none, and the local sync record. Creates nothing in Ronja. |
clone <module ID> [directory] |
Copies an existing module down into a new folder. Read-only — it does not open a draft. |
status |
What you changed locally, what the module looks like in Ronja, whether a draft is open and whose it is. |
validate |
Checks the folder on your machine and sends nothing: the file names, the sizes, the package name, the __init__.py, and the rule that a module carries no markers. |
push |
Syncs the folder into the module’s draft — creating the module itself on a first push. --force overwrites a draft that changed in Ronja since your last sync. |
publish |
Publishes the draft. A module in a shared feature that is waiting for its first approval is already with the Admins, and publish says so rather than sending it again. --overwrite-remote commits over a version somebody published while you were working. |
discard |
Throws the draft away; your local files are untouched. In a shared feature a module waiting for its first review is its draft, so discarding it deletes the module — that needs --delete-module. |
Every command takes --json and exits non-zero when it fails.
Only .py files are part of a module. Anything else in the folder is skipped, and each command lists what it left out so nothing disappears quietly. Data belongs in a table, a file or a Codex, not in the package. __init__.py is what makes the folder importable, so it is always there and cannot be deleted — empty it if you have nothing to put in it. A file can be up to 1 MiB and a module up to 256 files.
Import it from a workflow
Section titled “Import it from a workflow”In the workflow’s Python, write the marker where the package name would go:
from {{ module('module-abc123') }} import trackerimport {{ module('module-abc123') }} as emabRonja replaces the marker with the module’s package name when the workflow runs. Nothing else changes in the workflow folder: ronja wf push and ronja wf publish work as they always did, and the module’s files never appear in the workflow’s folder. That is the point — one copy, one version history, and a system that knows which workflows depend on it.
A module takes what it needs as function arguments. It holds no {{ ref }}, {{ secret }} or any other Ronja marker — the workflow resolves those and passes the database handle, the data and the configuration in. That is what keeps a module from quietly widening what a workflow can reach, and it is why a module can be versioned safely at all. A marker in a module file is refused when you save it, naming the file.
If the importing workflow is on runtime 3, the module’s code has to read tables the runtime-3 way — through tools.query(...) — because a module runs inside the workflow’s own container, and a runtime-3 container holds no table credential. A module that opens a table’s files directly is refused when you push or publish the workflow that imports it, naming the module and the file. The module itself is unaffected for workflows on runtime 1 and 2, which may go on importing it: the refusal is about the pairing, not about the module. Fix the module and publish a new version, then republish the workflow.
A module cannot import another module. If two modules share code, keep that code in one of them and call it with an argument.
A workflow in a shared feature may import only modules that are themselves in shared features: everyone who reviews or runs that workflow has to be able to read the code it executes.
Publishing a module changes nothing until you republish the workflow
Section titled “Publishing a module changes nothing until you republish the workflow”A workflow runs the module version its author last saved and tested with, and keeps running it. Publishing a new version of a module does not reach a single live workflow on its own — each one picks the new version up the next time it is published.
That is deliberate, and it is what makes a shared helper safe to edit: a fix cannot change what last night’s automation does behind your back, and a workflow an Admin approved goes on running the exact code that was approved.
So a change to shared code is two steps, and the second one is per workflow:
- Publish the module.
- In each workflow that should take the new version,
ronja wf pushandronja wf publish— republishing is what moves its pin.
ronja module publish tells you how many workflows import the module, so you know how much work step 2 is:
3 workflow(s) pin this module — each picks up the new version when it is next republished. Publishing a module changes nothing for a consumer on its own.Try a change before you publish it. Edit the module folder, ronja module push, then run ronja wf test in a workflow that imports it: a test run of your own draft uses your open module draft rather than the published version, so you can get a workflow and a module working together and publish both at the end. There is no ronja module test — a module has nothing to run on its own.
Publishing the workflow is the point where an unpublished module is caught: a workflow that imports a module nobody has published yet is refused with “publish module … first”. Save and test freely; publish the module before the workflow goes live.
Read a module in the app
Section titled “Read a module in the app”You write a module from the terminal and read it in the app. On the feature’s page a Modules lane sits beside Codexes, one row per module: its title, its Package column — the name workflows import — and when it was last changed. A feature with none tells you to create one from the CLI with ronja module init.
Open a row for the module’s own page. It carries the title, the description, the package name and when it was published, with a Details sidebar alongside and three sections under it:
- Files — every
.pyfile in the module, with its code. - Versions — the published versions, newest first, the newest marked Head.
- Used by — the live workflows that import the module (the first hundred, and a line says so when there are more), each linked, with the version that workflow pins: Latest, a date when it is still on an older version, or Not pinned yet while nobody has published the module.
Used by is where the two steps above stop being bookkeeping. Publishing the module moved none of those workflows; the ones showing a date are exactly the ones step 2 has left to do, and each reads Latest once you have republished it.
A workflow you have no access to still counts in Used by. It appears as a restricted row with no name and no link, so the number is honest without telling you what it is.
Nothing on the page edits anything. Changing a module is the loop above: ronja module clone to get it onto your machine, then ronja module push and ronja module publish.
Shared modules and who may change them
Section titled “Shared modules and who may change them”A new module in a shared feature is a proposal: anyone who can see the feature creates it, and an Admin approves it before any workflow can import it.
Where an Admin sees it. A module proposal does not appear in the Inbox on the Shared features page — that Inbox carries notes, workflows, and apps. What reaches an Admin instead is a notification: New module proposed, alongside Module change submitted for review for an edit draft and Module proposal decided for the outcome. Like every governance notification these start switched off; an Admin turns them on under Notification routing in the sidebar’s Governance group, in the Proposals and Reviews groups. Approving or rejecting is then done through the API — the CLI has no approve verb, so ronja module publish on a proposal tells you it is already with the Admins rather than deciding it for you.
Changing a live shared module is Admin-only, and this is the one place a module is stricter than a workflow. A shared workflow can be edited by whoever can see it and then submitted for review; a shared module cannot — an Admin checks the draft out, an Admin pushes to it, and an Admin publishes it. Ask an Admin to make the change, or keep the helper in your own private feature (where a shared workflow will not be allowed to import it).
Between Admins there is still a review step where one is wanted: an Admin who does not want to publish their own change runs ronja module publish and, where your organization does not allow approving your own work, the draft is submitted for review for another Admin to publish instead.
Publishing reports how many workflows import the module — the size of what was just changed. Since every one of those workflows keeps the module version it was last published with, publishing the module does not move any of them; each picks the change up the next time it is itself published.
A module cannot be moved to the trash while a live workflow still imports it. The refusal names the workflows; remove the marker from each one and save before trying again.
Related
Section titled “Related”- Create a workflow — the job a module is imported into.
- Use the Ronja CLI — signing in, profiles, stacks, and the
ronja wfloop this one mirrors. - Propose and review changes — drafts, proposals and who approves what.
- Versions, drafts, and approvals — the lifecycle every shared resource follows.