Python Executor: how we embedded Python in the automation of "First Form" without integrating it into the core

Business process automation has changed significantly in recent years. In the past, routing, rules, and simple business logic were often sufficient, but now more complex computational tasks, such as integrations with external AI services, are increasingly being integrated into processes.

In other words, automation is no longer just a reaction to an event; it is increasingly becoming a computational layer within the process itself. However, to ensure the system can handle the load, a powerful execution language is needed.

In this article, we will explain how we at "First Form" implemented this using Python. We embedded it into the platform's framework in such a way as to leverage its strengths for AI- and resource-intensive data processing scenarios while avoiding the execution of arbitrary Python code within the backend. For us, this was not a matter of "supporting another language"; we wanted to expand the platform without compromising the security and stability of the core.

Why the platform needed Python

When AI scenarios begin to emerge on the platform, not only does the business logic change, but so does the technical profile of automations. There are more tasks where it is necessary to not just check a condition, calculate a field value, or trigger a short action, but to process text, parse data, use NLP tools, and call external services. Often, this requires processing a massive array of corporate data.

For this class of tasks, Python has long become the de facto standard due to its ecosystem. Therefore, the question for us was not, "Does the platform need another language?" The question was different: how to safely provide the platform with Python for a new class of automations without turning the backend into an execution environment for arbitrary code with heavy dependencies.

This is where the architectural part of the story begins. The problem was not in adding a new language as such. The issue was in preventing Python from entering the process core.

Why Python could not simply be embedded into the backend

In many platforms, the first temptation is obvious: if there is already a scripting mechanism, then another can be embedded into the backend and executed just like the old ones. In the short term, this seems convenient. However, in the case of Python, this approach quickly creates systemic risks.

Python is not just the language itself, but also the execution environment, dependencies, packages, computationally heavier scenarios, and a much less predictable load profile than that of short scripts running in a process. If such code runs within the main backend process, it brings risks along with capabilities: memory leaks, heavy imports, unstable dependencies, user code errors, a more complex access control model, and a higher cost of failure.

For an enterprise platform, this is no longer a matter of implementation convenience, but a question of architectural boundaries. The more arbitrary user code is executed within the core, the harder it becomes to maintain system stability, predictability of updates, and a clear security model.

Therefore, for Python, we deliberately moved away from the model of executing code within a single process. We needed a separate controlled execution path that could be isolated, limited by contract, wrapped with timeouts, and developed independently from the core when necessary.

What we already had: two in-process models and a third, external approach

By the time Python appeared on the platform, we already had two smart scripting languages: Lua and JavaScript. They run inside the backend and are well-suited for internal automations where minimal latency and tight integration with the platform's API are needed. They provide quick startup, direct access to the platform's internal objects, and are suitable for cases where automation lives very close to the core.

Therefore, we deliberately integrated Python differently — through an external execution service. From the platform's perspective, this means something important: multiple execution models can coexist within one system for different classes of tasks. Lua and JavaScript remain a good choice for internal automations. Python is added not as a universal replacement, but as a separate tool for a different load profile.

To achieve this, we created an external service called Python Executor — a FastAPI microservice that accepts script code and context via HTTP, executes them in an isolated environment, and returns a JSON result back to the platform.

The execution route looks like this:

  1. The platform initiates the execution of the smart script.

  2. The backend determines the script language. If it is Lua or JavaScript, internal engines are used. If it is Python, the backend follows a separate branch and instead of local execution, sends an HTTP request to the Python Executor.

At the same time, the script itself is not stored in the service container but in the platform — in the tables SmartScripts and SmartScriptsVersions, along with other smart scripts. This is a fundamental point: the container does not contain the client's business logic and does not serve as a repository for scenarios. It receives code on the fly, executes it, and returns the result.

How the Python script execution contour is structured

From a technical point of view, the backend forms an HTTP request to the Python Executor and sends three main entities there: the script code, the execution context, and the timeout.

The main route for platform automation is POST /execute/code. In this mode, the service receives arbitrary Python code from SmartScripts, executes it, and returns a structure with the result, status, error text if necessary, string output, and execution duration.

In addition, the service has a second mode — POST /execute, where pre-installed scripts can be run by name. This mode is suitable for pre-prepared utilities. However, for the architecture of platform Python, POST /execute/code is more important because it makes Python part of the overall model of SmartScripts.

For this approach to be predictable, the Python script must have a very clear contract. Our entry point is the function execute(ctx). The script receives the context through the parameter ctx and returns the result using a regular return.

This is an important distinction from other scripting models, where the result can be specified via platform variables. For Python, we chose a more natural contract that aligns with the usual model of the language.

The context is formed by the backend with type filtering. Primitive values — strings, numbers, boolean values, dates — are passed as they are. If there is EntityBase in the context, only its identifier is passed to Python. Complex internal platform objects are not forwarded. The session_user_id is also automatically added.

And here lies one of the main limitations of the entire model: the Python script does not have direct access to the platform's API. It does not have access to the internal objects of the system for working with the database, HTTP requests, cache, and file system, which are available to internal in-process engines. It can only work with what the backend has explicitly placed in ctx, and with those libraries that are permitted and available in the container.

We plan to further develop this part — for example, by adding asynchronous execution with callbacks for more demanding scenarios. This is a natural step because AI and data processing tasks do not always fit well into a short synchronous model. But even in its current form, the scheme solves the main problem: the backend remains the orchestrator, while the Python Executor is the executor.

Why this is safer: isolation, sandboxing, and limited scope

Moving Python to a separate service is useful, but it is not enough by itself. Simply running arbitrary code in an external container without restrictions does not eliminate risks; they simply move to another process. Therefore, the next layer of architecture is sandboxes.

The Python Executor uses a white list model rather than a black list. The idea is that only a limited set of built-ins and modules is allowed, while potentially dangerous mechanisms are cut out in advance. In particular, calls like os.system, subprocess, eval, exec, import, write operations through open, as well as sys.exit, signal, breakpoint are prohibited. Direct access to the container's file system is also restricted.

This provides a much more manageable execution model than attempting to endlessly expand blacklists of dangerous paths. Whitelists are stricter, but they are better suited for a layer that executes user code.

Additionally, Python is also limited in execution time. The Python Executor uses a more conservative default timeout — 30 seconds compared to up to 5 minutes for internal Lua and JavaScript engines. This is related to the model of external HTTP calls: a synchronous request to a separate service should not block the backend for minutes. For heavy scenarios, an asynchronous mode with callbacks is planned, but in the current synchronous model, timeouts are stricter.

The conclusion here is simple: security is achieved not by a single mechanism, but by a combination of solutions — extracting from core, a separate process, a separate container, a limited context contract, a whitelist model, and a short timeout.

What a separate service provides in practice

A separate service also offers practical engineering benefits: it allows assembling the set of Python dependencies needed for AI and heavy scenarios in the environment without dragging the entire ecosystem into the backend.

The container has libraries for HTTP calls, tabular processing, Excel, SQL parsing, NLP, and AI integrations pre-installed. The documentation includes, in particular, requests, pandas, openpyxl, sqlglot, pymorphy3, nltk, rapidfuzz, openai. During the builder stage, dependencies that require compilation are installed, while the runtime stage remains lighter and does not contain a compiler. NLP data is downloaded when building the image, not at the time of script execution.

The estimated size of such a container is about 630 MB, but in this case, its size is a conscious price to pay for having a full-fledged Python runtime with ready-made libraries inside an isolated boundary. In return, the backend remains cleaner, and the Python layer becomes more independent and predictable.

Security is also built on the service call protocol here. The Python Executor accepts requests via an API key through the X-Api-Key header. The value is checked against the EXECUTOR_API_KEY passed through the environment. Sensitive data is not stored in the image permanently: for example, separate service tokens may come in the context of a specific call, rather than being stored inside the container as a permanent secret. This reduces the risk surface and better aligns with the isolated runtime model.

From an operational standpoint, the service lives as a separate Docker container, checks, logs requests, and can be updated independently of the platform core. This is important not only for DevOps convenience but also for the architecture as a whole.

The main engineering takeaway

When the platform starts to seriously integrate AI into processes, the main question is not about choosing the programming language itself. Much more important is where this code is executed, what data it receives, and what boundaries are set for it in advance.

We added Python to the platform not as just another built-in engine, but as a separate controlled runtime. With an HTTP call, limited context, sandbox, short timeout, API key, and isolated runtime. This allowed us to expand the platform for a new class of scripts without blurring the security boundaries of the core.

And perhaps this is the main point of the whole story. Safe extensibility is not when a new language "can do everything" and has full access to the entire environment. Safe extensibility is when the runtime for a new language is originally designed correctly: what it can do, what it cannot do, where it lives, and why its error should not become an error for the entire backend.

Comments