Skip to content

Automation Robots

Required Access Level

Admin or Super Admin

What is a robot?

A robot is an automated program that executes tasks without human intervention. Robots can:

  • Start processes automatically at scheduled times
  • Fetch data from external systems to feed Flowi Agentic
  • Execute steps that do not require human interaction
  • Integrate with ERPs, tax systems, and other platforms

Each robot is a pre-compiled JAR file uploaded to the platform, operating on a CRON schedule.

Attention: Robots ≠ Agents

"Robots" handle rigid systemic integrations (code). If you need cognitive automation (reading PDFs, interpreting free-form text, or making flexible AI-based decisions), refer to the AI Agents manual.

Listing Robots

Access Automation → Robots in the sidebar.

The list answers the question that matters at a glance — did anything break? — with one row per robot:

ColumnWhat it shows
Namethe robot's name and key, with a Global tag when the robot belongs to the platform
Statusactive or inactive
Schedulethe cron expression, or on demand when there is none
Last executionthe result and when it happened
JARthe active version, or a warning that one is missing

A robot that has never run reads Never ran rather than showing nothing: no result and a good result should not look alike.

Clicking the row opens the robot's page.

Registering a new Robot

  1. Click on New Robot
  2. Fill in:
    • Name — robot identification
    • Schedule — cron expression (e.g.: 0 8 * * MON = every Monday at 8 AM)
    • JAR File — upload the compiled file
    • Parameters — optional JSON configuration (the column is jsonb: a malformed object is refused on save, not on the first scheduled run)
  3. Click Save

Cron Expression

Format: minute hour day-of-month month day-of-week
Examples:

  • 0 8 * * * — every day at 8 AM
  • 0 8 * * MON — every Monday at 8 AM
  • */30 * * * * — every 30 minutes

A tick only fires if the previous one has finished

A robot on a one-minute cron that takes five minutes does not run five copies at once, and does not queue them either: the tick is skipped while the previous execution of that robot, in that tenant, is still running. The reason is logged by the application.

The manual trigger is not the exception: Run now while a run is in flight is refused, with a message saying so — see Manual Execution. The only difference between the two is what you see: the tick disappears into the log, the click gets the refusal on screen.

The check is per tenant: one slow subscriber of a global robot does not hold back the others.

The JAR is checked on upload

When the JAR is uploaded, the platform scans it and verifies that the registered main class exists inside it and implements FlowIaRobot. If it does not, the upload is refused straight away and the message lists the robot classes the file actually contains — usually it is a matter of copying the right name into the field.

The scan reads bytecode metadata and does not load the classes into the server process. Loading tenant-supplied code would run its static initialiser inside the platform, which is exactly what the sandbox exists to prevent.

Without this check, a mistyped main class would only surface when the robot ran — for a scheduled robot, hours later, as an execution failure rather than a registration error.

On the same upload the platform computes the file's fingerprint (SHA-256) and stores it alongside the version. Whenever the JAR has to be recovered — after a deploy, for instance, when the file is no longer on the server's disk — the platform checks that fingerprint before running it. If it does not match, the execution is refused with a message saying the file is not intact, instead of failing further along as "main class not found in JAR", which would point at the wrong thing.

Parameters declared by the JAR

A robot can declare the configuration it needs, on the class itself:

java
@RobotParameter(name = "erpUrl",   label = "ERP URL", required = true)
@RobotParameter(name = "erpToken", label = "Token",   required = true, secret = true)
public class SyncCustomerRecords implements FlowIaRobot { … }

The platform reads the declaration when the JAR is uploaded and builds the form from it, instead of asking for free-form JSON where a mistyped key only surfaces when the robot runs.

Filling in and saving that form — and even opening it to see what is stored — takes a tenant Admin or a Super Admin, the same role the rest of the robot registration requires. A manager or an ordinary user is refused: the configuration names every declared parameter and shows where a secret parameter's reference points, which is already half the way to the secret.

A secret parameter points at where the secret lives

A field marked secret = true should not hold the value. Write a reference into it, and the secret goes on living in the tenant variable that was built to hold it:

ReferenceWhere the value comes from
${var.erpToken}a tenant variable, including the ones marked secret
${config.erp.baseUrl}the tenant's configuration, by dotted path

The reference is resolved at the moment the runner asks for the parameters, inside the tenant's context. What is stored on the robot's registration is ${var.erpToken} — harmless in a backup, on a screen and in an audit log. Parameters no longer travel on the command line of the isolated process either: the sandbox fetches them over the same per-execution authenticated channel that already carries the logs.

A reference that does not resolve fails the execution and names the reference. It is never replaced by an empty string: a blank token would make the robot authenticate and report whatever the far end said about it, instead of failing where the problem is.

What is still not protected

Once resolved, the value is in the robot's memory — and the robot is code the tenant itself uploaded. It can log the value, send it anywhere or write it to a file, and no design prevents that short of not giving the robot the secret.

What this design does buy is narrower: the secret is not on the command line, not in the robot's stored configuration, not in the response of GET /a/robots, and not written by the platform into the execution log. Still use least-privilege, rotatable credentials, never the master password of a system — and, when writing a robot, never log the value of a parameter that could be a token.

The execution token left the command line as well: the agent hands it to the sandbox over standard input. That holds from the updated agent onwards — an older agent still passes the token as an argument, and when it does the execution log carries a warning saying exactly that. If you see that warning, update the runner-agent on that machine.

What the robot can reach is no longer everything either. Each robot declares its capabilities — reading its own parameters, writing its own log and reading and writing its own state always apply, which is why they have no tick box on the screen; reading or writing the CMS, starting a process, querying instances, listing members and uploading a file are ticked one by one. The execution token carries only what was ticked, and the route refuses the rest with a 403, naming the capability that is missing and where to tick it.

A robot that already existed was given every capability on upgrade, so no automation stops working unannounced. A new robot is born with the minimum and you tick what it needs — on the creation form, and afterwards on the robot's own screen under Save access. If an execution fails saying a capability is missing, that is where it is fixed, and the message says which one.

A global robot is not editable by the tenant: the platform maintains it, and its capabilities come with it.

Either way, treat the runner machine as a trusted machine: whoever has an account on it can read the process's memory, and no design prevents that.

Global robots and each tenant's configuration

A platform robot is visible to every tenant, but there is one JAR and there cannot be one configuration. Hence the split: the JAR declares the shape, each tenant supplies the values.

On a global robot's page a tenant administrator fills in the declared fields and saves. Those values apply only to that tenant.

The same form serves a tenant-owned robot — the difference is only where the value is stored: on the robot, because it has a single owner. Nobody using the screen has to know the difference.

Changing the robot itself — name, schedule, main class, uploading a new JAR, deleting — belongs to the platform administrator. For a tenant administrator those commands are not shown on a global robot's page: in their place is a notice saying whose change it is and pointing at the subscription form just below. They used to be shown and answer with a refusal; offering a button that can only refuse is worse than not offering it.

What stays available is Run now, and not by oversight: a global robot only runs through the tenant's subscription, so running it is precisely what the tenant administrator can do with it. For the platform administrator nothing changes — every command is there.

In the list and at the top of the page, the Global tag identifies a platform robot, the same way global processes are already marked.

A global robot only runs inside a tenant

A global robot is the JAR's registry entry, not something that runs by itself. There is no "global" execution: every run happens inside a tenant that subscribed to the robot, with that tenant's parameters.

So a tenant that never filled in the configuration does not run the robot — not from the button, not on a schedule. The refusal names the tenant and where to configure it. A tenant that switched its own subscription off does not run either, however hard someone presses the button.

The default values shown on the global robot are a template offered to whoever subscribes, never what executes. Were they used at execution time, every subscriber would run with the platform's configuration — and, for a secret field, with its credential.

When a new version asks for more than the previous one

If the platform administrator uploads a JAR declaring a new required parameter, every subscription without that value becomes incomplete. The upload succeeds — a new version has to be publishable — but the robot does not run in that tenant until someone fills it in, neither on schedule nor by button.

That is deliberate, and it is the choice between two bad things: a robot stopped with the reason written down, or a robot running on half a configuration and reporting success. The second is worse, because nobody goes looking.

Scheduling a global robot

The schedule belongs to the robot, and each tenant can switch its own off. At the scheduled time the platform fires one execution per subscribing tenant — ten subscribers, ten executions, each in its own tenant's context and with its own parameters. One failing does not interrupt the others, and an incomplete subscription is skipped with the reason in the log.

Activating and Deactivating

Use the active/inactive toggle in the list to control whether the schedule is running. A deactivated robot will not execute but remains registered.

The robot's page

Opening a robot shows its main class, schedule, parameters and active JAR version, and that is where the JAR upload happens — after the robot exists, because the file is versioned per robot.

Below it is the executions table: status, trigger, start, duration and the error when there was one. While any execution is open — PENDING or RUNNING — the table refreshes itself every few seconds, and stops asking once they all finish. There is no need to reload the page after clicking Run now. Clicking an execution opens its log, and the dialog carries Download .log: a text file with the whole log in the order it happened, each line marked with where it came from. Good for grep, for attaching to a ticket, and for reading away from the screen.

The complete file is sealed into object storage when the execution finishes — the runner uploads it, and the platform confirms it arrived before trimming the database, which then keeps only the last lines for the screen. An execution that has not been sealed downloads straight from the database: the button works either way. The log lines are fetched only at that point; the list does not carry them, so opening the page never pulls every line of every execution.

Manual Execution

Click Run now to trigger the robot outside of its schedule. Useful for testing. The button stays disabled while the robot has no active JAR — without one there is nothing to run.

One run at a time, per robot and per company

If this robot already has a run in flight in this company, the manual trigger is refused with that message. Until 2026-09-02 it started anyway, which made sense while a run remembered nothing. With the State kept between executions (below), two simultaneous runs read the same position, process the same stretch, and whichever writes last wins — records go missing with no error at all. Wait for the current run to finish, or cancel it.

State kept between executions

A robot walking an incremental feed — the next page of an API, the last id already imported — has to remember where it stopped. That is the State: key/value pairs the robot writes during a run and reads on the next one.

State is per robot and per company. On a global robot each subscribing company has its own: ten subscribers do not share one pagination position, each resumes where it itself stopped.

It is not the same thing as the declared parameters, and the difference matters: a parameter is what a person decided and typed into the form; state is what the robot found out on its own. That is why they are two separate things on the screen — a robot does not write into the administrator's form, and an administrator editing the form does not overwrite the robot's cursor.

Each write by the robot lands immediately. If it records the page after processing each one and dies on page 7 of 20, the next run starts at 8 — not at 0.

On the robot's page, the State kept between executions card shows the current content as JSON. You can edit and save it (the day the source system renumbers its pages, this is where it gets fixed) or use Clear state to wipe it. Without that, a wrong value would only come out through database access.

Two bounds, stated on purpose:

  • 64 KB for the whole state, keys and values added together — not a per-key cap. State is where the robot notes where it stopped, not where it keeps the result. The write that would take the set past the cap is refused, with the size and the limit in the message, and the previous value stands. Volume goes to the CMS or to storage; the pointer stays here.
  • Not a place for secrets. State is not masked and it shows on this screen. Tokens and passwords go into a tenant variable, referenced by a secret parameter.

Deleting the robot deletes its state with it.

Cancelling an execution

Every execution that is still open — no finish time, whether PENDING or RUNNING — carries a Cancel run button on its own row in the table. A finished execution offers no button: there is nothing to cancel on a closed row, and reopening it would erase the outcome the runner reported.

The confirmation says which of the two cases yours is, because they are not the same thing:

SituationWhat confirming does
PENDING — no runner has collected it yetSettled immediately. The execution leaves the queue, becomes CANCELLED, and never runs
RUNNING — a runner already collected itThe request is recorded now, the row is marked Cancel requested and stays RUNNING. The agent on that machine finds out at its next heartbeat, stops the robot, and reports CANCELLED

Since the table already refreshes itself while any execution is open, the state change shows up without a reload. Who cancelled and when the request was made are both recorded.

Cancelled is not a failure. The execution ends with status CANCELLED, with no error message and no failure notification to whoever triggered it. Reading a cancellation as a failure sends someone to debug a robot that did exactly what it was told.

The limit: the platform does not reach inside the customer's machine

Cancelling a running execution is a request the agent has to come back for. If that agent never calls again — machine off, network cut, process killed — the robot may still be running on a machine the platform does not administer.

What the platform always does: it stops dispatching that execution and stops counting on it. The lease expires, the row is closed as CANCELLED with the explanation written on it, and the runner stops being treated as busy on its account. What it does not do is end a process on someone else's server — there is no path for that, because the platform never opens a connection into the customer's network; it is always the runner that calls out. Whoever administers that machine is who ends the process.

Monitoring Executions

The execution history for each robot shows:

  • Start and end date/time
  • Status: SUCCESS, FAILED or CANCELLED
  • Output log or error message

In case of failure

Robot failures are logged but do not interrupt other processes. Check the log to diagnose the issue and fix the JAR before reactivating.

When the runner's machine disappears mid-way

Two different things can happen, and the platform treats each one differently — because the risk is not the same.

The machine stopped responding before fetching the work. The robot never started. The execution goes back to the queue and the platform hands it to another eligible runner as soon as one is available. Nothing to do.

The machine stopped responding with the robot already running. Here the platform does not re-dispatch, deliberately: it cannot reach inside the process and has no way of knowing whether it is still alive on the customer's machine. Running the same robot again would make both executions read the same saved position in State, process the same stretch, and the last one to write would win — records disappear with no error at all. So the execution is closed as FAILED, with the reason written on it, and the robot is free for the next run. If the work really does need redoing, trigger the robot from the screen after checking what the interrupted execution managed to write.

The log has two sources, and the screen keeps them apart

  • [INFO], [WARN], [ERROR] — what the robot chose to record, by calling context.log(...).
  • [process] — what the JVM printed: System.out, System.err, stack traces. It is shown indented and dimmed, because reading a stack trace as if the robot had written it on purpose leads to the wrong conclusion.

The two travel together, in batches during the run rather than only at the end, so they appear on screen in the exact order they happened: the context.log("about to download the file") sits right above the stack trace explaining why the download failed.

The level of a context.log(...) is one of TRACE, DEBUG, INFO, WARN and ERROR. Anything else is recorded as INFO.

Consecutive lines of the same source and level appear under a single [INFO], and line breaks are preserved: a context.log(...) carrying a stack trace stays readable line by line instead of collapsing into one paragraph.

A robot that prints too much keeps its head and tail, with a line in between saying how many were dropped. The head explains the configuration; the tail explains the failure. The ceiling covers both sources together, so a robot that logs heavily cannot push out the stack trace that explains its own failure.

What the robot prints reaches the screen verbatim

Nothing masks this output. A robot that prints a password puts that password in the execution log and in the database. Record what happened, never the credential used.

context.log(...) does not depend on the network

Log lines leave the robot on the child process's standard output, and the runner agent delivers them to the platform in the same batch as the process output. Two practical consequences:

  • A robot killed at its timeout does not take its log with it. By then the agent holds the lines, not the process that was just killed.
  • context.log(...) does not fail because of the network, and does not retry. The guarantee it gives is "the agent shipped the batch", not "the platform acknowledged this line".

The other RobotContext calls talk to the platform and fail the execution when they cannot: writing to the CMS, starting a process, uploading an attachment. A robot that could not do what it was run to do does not finish as Success.

Runners — the machine a robot runs on

A runner is the machine that actually executes a robot. The platform schedules, assigns and shows the result; what opens the JAR and runs the code is always a runner.

Every robot runs on a runner. The platform schedules the work and shows the result; what opens the JAR and executes the code is always a machine registered as a runner. That is what keeps one client's code off the disk holding the other clients' configuration, credentials and attachments.

The practical consequence is direct: with no runner registered and answering, executions stay PENDING and the screen says why, instead of running somewhere nobody chose.

A Helm installation already has one. Since 2026-08-29 the chart brings up runner.replicaCount runners (1 by default) that generate their own key and enrol themselves as shared runners — runner.replicaCount is also the parallelism knob, because a shared runner runs one execution at a time. Outside Helm, registering at least one runner is part of bringing the installation up.

Why register a runner of your own

To reach a system inside the customer's network. A robot almost always exists to talk to an ERP, a fiscal system or an internal API. With no runner of your own, it is the platform that has to reach that network: an inbound firewall rule, a tunnel, or an exposed ERP. A runner installed inside the customer's network makes only outbound connections, and the question disappears instead of being answered.

To keep one customer's code off another customer's machine. On the shared runner, one tenant's JAR runs on the same machine as another's — exactly the exposure described in the What the sandbox does not isolate yet: the disk warning below. A runner of your own takes that customer's code out of there. If every tenant on your installation is a different customer, this is the strongest reason on the list.

A robot can refuse the shared runner

On the robot's page, the Runner field says where that robot accepts running:

ValueWhat it means
Any available runnerthe default — the installation's shared runner included
Dedicated — refuses the shared runneronly runners belonging to this tenant

Marking a robot dedicated is a security decision, not a performance one: it is what keeps that customer's JAR off the same machine as another's. The price is that with no tenant runner available the execution stays pending instead of falling back to the shared one — and the executions screen says so, rather than quietly running it elsewhere.

Registering a runner

Go to Automation → Runners in the sidebar — it is a sibling of the robot list, not part of the platform area: whoever manages a tenant's robots manages that tenant's runners too.

  1. On the machine, generate the key pair:

    java -jar flowia-runner-agent.jar --init

    The command prints the public key and its fingerprint. The private key never leaves that disk.

  2. Click New Runner, give it a name (e.g. erp-fiscal-01), paste the public key and pick a scope.

  3. On the machine, start the agent:

    java -jar flowia-runner-agent.jar --url https://<your-installation> --name erp-fiscal-01

The screen has a copy button on each of the two commands.

The agent checks, on startup, that it can reach the platform. Before claiming any execution it tries the RPC address the platform gave it. If it cannot reach it, it stops there and says which address it tried. It is nearly always platform configuration: app.api.base-url must be the platform's address as seen from outside itlocalhost there is the platform itself, never the runner's machine.

The key is born on the machine, so nothing secret travels

The platform stores only the public key — which is public by definition. This screen never shows, generates or hands over a token: if you are looking for a secret to copy, the direction is the opposite of what you expect. A public key pasted in the wrong place leaks nothing.

Scope — what that machine accepts

ScopeAccepts
Every robot of this tenantany robot belonging to the tenant that owns the runner
Selected robots onlyjust the robots picked when it was registered

Every robot of this tenant is the ordinary case: a machine inside the customer's network that may run whatever that customer's robots are. Selected robots only is for the machine that is special — the one that reaches the fiscal system, or the one with a licence dongle attached — where "runs everything" is wrong and the list is short and deliberate. Picking that scope makes the screen ask which robots.

The runner's own log

Every row carries Log: what that machine's agent reported about itself — a refused handshake, a JAR digest that did not match, an execution collected, the child's exit code, a reconnection after an outage. It is what answers "why is my runner not picking up work" without access to the machine.

It records events, not routine: an idle agent asking for work every 25 seconds writes nothing. And what happened while the platform was unreachable does not arrive — the agent holds the last lines and sends them when it comes back, but a long silence shows up as silence.

The shared runner's log belongs to the platform

It names executions and main classes of every tenant it served, so only the platform administrator reads it. Your tenant's execution logs stay on the robot screen.

The list and each runner's state

ColumnWhat it shows
Namethe name given when it was registered, and the machine's Java version
Scopewhat that runner accepts, and which robots when the list is explicit
Fingerprintthe public key's fingerprint, to check against what --init printed
Last seenwhen the agent last polled
Statusonline, offline, waiting for first connection, or revoked

"Waiting for first connection" is normal, not a failure

Registration comes before the machine: you register the public key and only then start the agent — sometimes days later, sometimes on a machine that has not been provisioned yet. Until then the runner shows as Waiting for first connection, and leaves that state on its own as soon as the agent polls for the first time. There is nothing to fix.

Replacing the key, and revoking

Rebuilt machine — run --init again on the new machine and paste the new key into Edit runner. Filling the key in revokes the old one by construction; there is no need to delete and register again.

Revoking — the Revoke button asks for confirmation and says what happens: the runner fails its next poll and stops receiving work, and an execution already running is not killed. Revoking kills nothing mid-flight; it only stops that runner from being picked from then on.

JAR Development (Native API)

The JAR loads into a URLClassLoader of its own, inside the child process, and talks to the platform only over RPC authenticated with an ephemeral token for that execution. If the robot hangs, eats all the memory or calls System.exit, the process that dies is its own; the platform records the failure and carries on.

What the sandbox bounds:

BoundWhat it means for tenant code
Environment variablesThe process gets only the minimum the JVM needs to start. Database credentials and AI provider keys are not visible
MemoryStarts with -Xmx (512 MB by default, configurable); a runaway robot dies on its own
TimeOn timeout the process is killed, not merely recorded as expired
Working diskA working and temp directory for that execution only, deleted when it ends, failures included. A file in /tmp does not survive to the next execution
ReachOnly what RobotContext exposes, always within the robot's own tenant

What the sandbox does not isolate yet: the disk

The robot process runs as the same OS user as the platform, and therefore with the same file permissions. Environment, memory and run time are bounded; the filesystem is not.

If the installation's tenants represent distinct parties — your firm's own clients, say, one tenant each — that means code uploaded by one tenant can read files the platform can reach, including another tenant's. This is not a theoretical limitation and there is no setting that turns it off.

Worth stating because the natural reading is the opposite: an installation belonging to a single company does not make this smaller. When each tenant is one of your clients, the boundary between them is a confidentiality obligation between distinct parties — stricter, not looser.

Real isolation requires robot execution to run in its own container — the other three routes do not work on this deployment model (dropping privileges needs the platform to run as root, an OS sandbox needs capabilities the managed runtime withholds, and SecurityManager was removed in Java 21). That work has not been implemented yet.

Until then: treat JAR upload as a permission of trust. There is no cross-tenant exposure only when tenants are divisions of one organisation. If each tenant is a different client, grant tenant ADMIN only to someone you would trust with read access to the server — in practice, only your own team.

Accessing data, CMS and process variables (JSONB)

Robots can interact richly with process instance data (using variables structured as JSON/JSONB) and also with Flow.IA's schemaless database (the CMS).

To build your robot, implement the FlowIaRobot interface:

java
import ia.flow.engine.FlowIaRobot;
import ia.flow.engine.RobotContext;
import java.util.Map;

public class MyRobot implements FlowIaRobot {
    @Override
    public void execute(RobotContext context) {
        context.log("Scanning CMS...");

        // Direct access to JSON Parameters provided in the UI
        Map<String, String> params = context.getParameters();
        
        // Native API Access: Consume or write to the current Tenant's CMS
        context.getCms().getRecordsByCollectionSlug("invoices")
               .forEach(record -> context.log("Processing invoice: " + record));

        // Native API Access: Start new processes
        context.getProcesses().startProcess("billing-workflow", Map.of("source", "robot"));

        // Native API Access: Query organizational members
        var members = context.getTenant().getMemberEmails();
        context.log("Members notified: " + members.size());

        // Native API Access: Upload files (e.g. attaching to a process instance or task)
        byte[] pdfContent = new byte[]{ /* ... file bytes ... */ };
        java.util.UUID attachmentId = context.getStorage().uploadAttachment(
            "instance", "instance-uuid-here", "invoice.pdf", "application/pdf", pdfContent
        );
        context.log("Uploaded invoice attachment: " + attachmentId);
    }
}

The RobotContext is your isolated facade to Flow.IA. It guarantees that any operation (like getCms(), getProcesses(), or getStorage()) is strictly restricted to data belonging only to the active robot's Tenant, preventing a robot from leaking data from neighboring companies on the platform.

Where the annotation comes from

@RobotParameter is not published to any Maven repository: declare your own copy, in the same package and under the same name. The annotation's source and the reasoning are in Java Delegates → Where the annotation comes from — the only difference is that @RobotParameter has secret and @DelegateParameter does not.

Flowi Agentic — Plataforma de Gestão de Processos com IA