PYTHON_SANDBOX_RUN for a step that needs Python code. The required code input runs in a sandbox. The value of the last expression is the result, with text printed to stdout as the fallback; data_type controls whether it is parsed as STRING (default), NUMBER, or OBJECT. For OBJECT, end the code with json.dumps(...) as the last expression (or print it with print(json.dumps(...))): a bare dict or list comes back as a Python repr, which is not valid JSON.
Calculate a number from earlier steps
result.data, which should be 3 in this example. Inspect logs and error if the step fails. A NUMBER result must parse as a number; use STRING for ordinary text or OBJECT for JSON-compatible data.
Install packages and return an object
Add Python packages in the step’sconfig when the code needs a library. This example totals sales rows from an earlier step with pandas and returns a JSON object.
OBJECT, the JSON printed to stdout is parsed, so later steps read {{steps.summarize.result.data.grand_total}} (250) or {{steps.summarize.result.data.totals.east}} (170). Wrap step output in json.loads(r'''...''') as shown: pasting JSON directly into Python code breaks on null, true and false.
In an organization with an egress policy
In an organization with an egress policy (a restriction on which external hosts sandboxes may reach), the sandbox is locked down before your code runs, and the code runs without administrator rights:- There is no
sudo,/usr/localis read-only and/rootis not accessible. Install system packages with thedependenciesconfig, which runs before your code with the rights it needs. packagesinstall before the lockdown, so they need no allowed host. Apip installin your code runs after it and needs the package index (pypi.org,files.pythonhosted.org) on the allowed hosts. Both install into the user’s own site directory (~/.local) and import as usual; for pip inside a virtual environment you create yourself, setPIP_USER=0.- Every external hostname resolves to the placeholder address
198.18.0.1inside the sandbox. HTTP and HTTPS requests to allowed hosts still work, because the sandbox routes them by hostname; code that needs a host’s real IP address does not, and a request to an IP address instead of a hostname is refused. - Only HTTP and HTTPS (ports 80 and 443) to allowed hosts get out. Other protocols, and plain DNS queries, are blocked.
Read step data and secrets
- Previous step data is available as
steps.{step_name}.result.{key}; secrets assecrets.{secret_name}.{key}(e.g.secrets.OAUTH.access_token). For single-key secrets,str(secrets.{secret_name})returns the value directly. - Only the secrets your code names are sent to the sandbox. Reference each one by a literal name —
secrets.NAME,secrets["NAME"]orsecrets.get("NAME")(f-string fields such asf"Bearer {secrets.NAME}"count). A computed key (secrets[name]) or a whole-map read such assecrets.items()orjson.dumps(secrets)is rejected before the sandbox starts.
Large tables: tables
A step output travels between steps as JSON. A DataFrame with hundreds of
thousands of rows does not survive that: serializing it exhausts the sandbox’s
memory before the step can return.
Every Python step is given a tables helper for this. tables.write(df)
publishes the frame to storage and returns a small marker — a few hundred
bytes naming the stored table — which becomes the step’s result. The next step
calls tables.read(...) on that marker and gets the DataFrame back. The rows
never become JSON.
INPUT_FILE returns the file URL as a string, so steps.csv_file.result is
the URL itself and pd.read_csv reads it directly.
The marker is a dict, so the step has to return it the way it returns any
other structured result: json.dumps(...) as the last expression, with
data_type: OBJECT. Without that the step’s data_type defaults to STRING
and the next step receives the dict’s Python text, which tables.read cannot
open.
tables.write(df, name="result", fmt="csv", index=None) publishes a frame
and returns its marker.
name— the stored object’s name, so one step can publish several tables. Letters, digits,.,_and-only, with no..and no leading or trailing dot.fmt—"csv"(the default) or"parquet". Parquet needspyarrow, which is not installed in every sandbox image;tables.writesays so if it is missing.index— whether to write the frame’s index as columns. The default,None, writes it when it carries data: the grouping keys ofgroupby(...).agg(...)orpivot_table, aDatetimeIndexfromresample. A plain positional index is dropped, so a filtered frame does not gain a stray column. PassTrueorFalseto decide it yourself.
values first:
df.columns = ["_".join(str(part) for part in column) for column in df.columns.to_flat_index()].
df must be a DataFrame, not a Series. df.groupby([...])["amt"].sum() returns
a Series — call .to_frame() on it first; its grouping keys are a named index
and are written as columns.
tables.read(marker) returns the DataFrame. The frame comes back flat:
everything tables.write serialized is a column, including levels that were
an index on the way out. marker["__jinbatable"]["columns"] lists them, along
with rows and sizeBytes, so a later step can see the shape without
fetching the table. rows is also checked against the frame that actually
parsed: an object cut short mid-upload is refused here rather than handed on
as a quietly shorter table.
tables.is_table(value) reports whether a value is a marker, for a step
that accepts either a table or inline rows.
Size limit. One tables.write call is bounded by what the deployment
accepts in a single upload, and that bound belongs to the deployment rather
than to the tool: on the managed service the platform ingress rejects a request
body over 32 MiB, while a self-hosted deployment may have a different
ceiling, or none, and may instead cut a slow upload on a request timeout.
tables.write reports whichever happened — a frame refused for its size, or a
connection closed before any status came back — and names the limit when the
deployment has declared one. Either way: aggregate or filter before writing, or
publish several smaller tables with different names. Reading is not bounded
this way: a table already in storage can be read back whole, subject only to
the sandbox’s memory — which for a very large frame is the binding constraint,
because tables.read materializes the whole table at once.
Notes:
- A table is readable by later steps of the same flow, including
reusedsteps replaying an earlier run’s output. It is not readable from another flow. - Inside a
forEach, each iteration gets its own storage slot, so every iteration can calltables.write(df)with the default name without overwriting its siblings. tables.readalso accepts a marker arriving as theitemof aforEach.- Small results need none of this: a step that returns rows still returns rows.