Custom Integration Scripts
Custom integration scripts let you import asset data from any source that runZero does not already integrate with, or export runZero data to another system. Scripts are written in Starlark, a Python-like language, and run in a sandbox on one of your Explorers or on the runZero console itself.
runZero 5.1 significantly expanded what custom integration scripts can do:
- An embedded
CONFIGblock describes the integration and its parameters. The console uses it to generate a typed credential form, validate input, apply defaults, and route secrets through encrypted storage. - Shared TLS and HTTP option suites (
OPTIONS_TLSandOPTIONS_HTTP) add standard connection controls to any script without copying boilerplate parameters. report_assetsstreams assets to runZero page-by-page, so large imports no longer need to be held in memory.- A CONFIG-level
matchBehaviorpolicy and per-assettrust_*flags give scripts control over how imported assets merge with existing inventory. - A much larger library of built-in modules, including typed
kwargsaccessors, HTTP helpers with retry support, parsers for CSV/XML/streaming JSON, JWT and AWS SigV4 signing, and direct-protocol modules for SSH, SMB, WinRM, WMI, and SQL. - The
runzero scriptCLI command gained a--validatemode that smoke-tests a script’s CONFIG and HTTP/TLS wiring against a local dummy server, and an--outputmode that writes real scan output for inspection. - The custom integration page in the console became a script workbench with an editor, test runs against real endpoints, a shipped example library, version history, and — where AI is enabled — a generative AI assistant that drafts and revises scripts.
A library of ready-to-use integrations built on these capabilities is available in the runzero-custom-integrations GitHub repository, including a boilerplate template to start from.
Overview
To set up a custom integration script:
- Write the script, starting with a
CONFIGblock and amainfunction. - Test it locally with the runZero CLI (
runzero script). - Create the custom integration in the console and paste in the script.
- Create a credential; the form is generated from the script’s
CONFIGparameters. - Create an integration task to run the script on a schedule.
Writing a script
The Starlark language
Starlark is a dialect of Python with some notable differences:
- There is no exception handling (
try/except). Use return values to signal errors. - There is no f-string
f'{var}'formatting. Use"{}".format(var)for string interpolation. - The standard library is limited to the built-in modules provided by runZero.
The custom integration sandbox also disallows recursion, top-level if/for statements, and reassigning global variables. while loops and the set() type are supported.
The entrypoint
Every script must define a main function. runZero calls it with the task’s arguments and keyword arguments:
load('runzero.types', 'ImportAsset')
def main(*args, **kwargs):
asset = ImportAsset(
id='device-1',
hostnames=['web1.example.com'],
os='Linux',
osVersion='6.1',
manufacturer='Example Corp',
model='VM',
)
return [asset]
argsreceives any positional arguments configured on the task, as strings.kwargsreceives the credential fields and task keyword arguments, as strings. Declare every key your script reads inCONFIG["params"], and read them with the typedkwargsaccessors.- For inbound integrations,
mainreturns alistofImportAssetobjects (or a singleImportAsset). ReturningNoneis valid when assets are streamed withreport_assetsinstead. - For outbound integrations,
mainperforms its export work and returnsNone.
Streaming large imports with report_assets
Returning one large list from main keeps every asset, plus the raw API responses used to build them, in memory at once. For sources with large inventories, stream each page to runZero as it is processed instead. report_assets is a predeclared builtin, so no load() is needed:
load('runzero.types', 'ImportAsset')
def fetch_page(cursor):
# Replace with a real paginated API call.
if cursor == None:
return [{"id": "a-1"}, {"id": "a-2"}], "page-2"
return [{"id": "b-1"}], None
def main(*args, **kwargs):
total = 0
cursor = None
while True:
page, cursor = fetch_page(cursor)
if not page:
break
assets = [ImportAsset(id=item["id"]) for item in page]
total += report_assets(assets) # stream this page to runZero
if not cursor:
break
print("reported {} assets".format(total))
return None # nothing buffered in main
report_assets accepts a single asset, several positional assets, or a list/tuple of assets, and returns the number reported. Assets streamed with report_assets and assets returned from main are both imported, so partial adoption is safe.
The singular report_asset (also predeclared) takes exactly one ImportAsset (or None, a no-op) and returns 1 or 0. Streaming already batches internally, so reporting each asset as it is built is the simplest correct pattern — there is nothing to gain by accumulating a page into a list first:
for device in devices:
report_asset(build_asset(device))
For the pagination loop itself, the predeclared pager builtin guards against a cursor that never advances. p = pager(label="devices") returns an object whose p.next() is used as the while condition; if the loop reaches the page ceiling (maxPages, default 1,000,000), the script stops with an error naming the label instead of silently truncating or spinning forever. max_pages() returns the effective ceiling.
The CONFIG block
The CONFIG block is an embedded, declarative description of the integration. When you save a script in the console, runZero extracts CONFIG (without executing the script) and uses it to:
- Render a typed credential form with labels, groups, and conditional fields.
- Validate submitted values (
required,min,max,pattern,options) and applydefaultvalues beforemainruns. - Route
secretparameters through encrypted storage and redact their values from script logs and error messages. - Refuse to run the script on runZero versions older than
minVersion.
CONFIG = {
"id": "runzero-example",
"name": "Example integration",
"type": "inbound",
"description": "Imports devices from the Example API.",
"version": "1",
"minVersion": "5.1.0",
"params": [
{"key": "url", "label": "Example API URL", "type": "url", "required": True},
{"key": "api_token", "label": "API token", "type": "secret", "required": True},
{"key": "page_size", "label": "Page size", "type": "int", "required": False,
"default": 100, "min": 1, "max": 1000},
],
"includes": {
"tls_": OPTIONS_TLS,
"http_": OPTIONS_HTTP,
},
}
load("runzero.types", "ImportAsset")
# ...rest of script
Rules for the block itself:
CONFIGmust be the first top-level statement in the script. Comments and blank lines may appear before it;load(...)calls, constants, and other statements may not.- Every value must be a literal: strings, numbers,
True/False/None, lists, tuples, and dicts with string keys. Function calls, variable references, and arithmetic are rejected. The only exception isCONFIG["includes"], which may reference the predeclared option-set identifiersOPTIONS_TLSandOPTIONS_HTTP. - Scripts without a
CONFIGblock continue to run with the legacyaccess_keyandaccess_secretcredential fields, so existing integrations keep working unchanged. sourceIdandsourceNameare reserved for runZero’s own shipped integrations. A script that declares either is refused when you save it.
Top-level fields
| Field | Description |
|---|---|
id |
Stable lower-case identifier, for example runzero-tailscale. |
name |
Display name for the integration. |
type |
inbound, outbound, or internal (descriptive). |
description |
Short summary of what the integration does. |
version |
The script’s own revision, bumped when the script changes, for example "1". |
minVersion |
Minimum runZero version required to run the script, for example 5.1.0. Older Explorers refuse to run the script and report a clear upgrade message. |
params |
List of parameter definitions. |
includes |
Shared option suites, keyed by prefix. |
atLeastOneOf |
List of parameter-key groups; at least one key in each group must be set. |
exactlyOneOf |
List of parameter-key groups; exactly one key in each group must be set. |
rejectUnknown |
Reject keys that are not declared in params. Unknown keyword arguments are always rejected at run time for CONFIG-based scripts. |
validationMode |
How runzero script --validate exercises the script: the default expects an HTTP request; "compile" only checks that CONFIG parses and main exists. Use "compile" for direct-protocol (SSH/SMB/WMI/WinRM/SQL) integrations. |
maxPages |
Page ceiling enforced by the predeclared pager builtin; defaults to 1,000,000. A script may lower the effective ceiling by passing limit= to pager, but not raise it. |
matchBehavior |
Integration-wide merge policy, a space-separated string of flags. |
assetType |
Asset type applied to every asset the integration reports, overridable per asset with the assetType field on ImportAsset. |
assetTypeBehavior |
Map of asset type to a matchBehavior-style flag string, layered over the integration-wide matchBehavior for assets of that type. Also accepted as sourceTypeBehavior; declaring both with different values is an error. |
ownershipAttributes |
Attribute keys the integration supplies that should be treated as ownership data. |
trustOS, trustOSVersion, trustType |
Trust this integration’s OS, OS version, or device type over other sources, for every asset it reports. The per-asset trust_* fields on ImportAsset do the same thing for one asset. |
Parameter definitions
Each entry in params describes one credential form field and one keyword argument delivered to main.
Supported type values: string, secret, int, float, bool, enum (requires options), url, textarea, and json.
| Field | Description |
|---|---|
key |
Keyword argument name. Must match ^[a-zA-Z_][a-zA-Z0-9_]*$ and match the name the script reads. |
label |
Form label. |
description |
Help text shown under the field. |
type |
One of the types above. secret values get masked input, encrypted storage, and log redaction. |
secret |
Treat the value as a secret regardless of its type. Equivalent to type: "secret" for masking, storage and redaction. |
required |
Whether the field must be set. |
default |
Default value applied when the field is left blank. Not allowed on secret parameters. |
placeholder |
Placeholder text for the form field. |
options |
Allowed values for enum parameters. |
multi |
For enum: allow multiple comma-separated selections. |
min / max |
Numeric bounds for int/float; length bounds for string/secret/textarea. |
pattern |
Regular expression the value must fully match (string, secret, textarea, url). |
aliases / caseInsensitive |
For enum: alternate spellings that normalize to a canonical option before main runs. |
dependsOn, visibleIf, visibleIfValue |
Show this field only when another declared field is set (optionally to a specific value). |
requiredIf, requiredIfValue |
Make this field required when another declared field is set (optionally to a specific value). |
group |
Section heading used to group fields in the form. |
How values reach your script
- Each declared parameter arrives in
kwargsunder itskey. Values are delivered as strings; use thekwargsmodule (get_string,get_int,get_bool,get_list, …) to read them with type coercion and defaults. - Declared
defaultvalues are applied, enum aliases are normalized, and validation runs beforemainis called. - CONFIG-based scripts reject unknown keyword arguments.
- Credential keys starting with
_(for example_integration_id) are stored on the credential but never forwarded to the script. - For backward compatibility, the credential’s
access_keyandaccess_secretfields are still injected when the task does not provide them.
Shared TLS and HTTP option suites
Most integrations talk to an HTTPS API, and most of them need the same connection controls: trusting a private CA, pinning a certificate, presenting a client certificate, disabling validation in a lab, or sending a specific User-Agent. Rather than declaring these parameters in every script, add the predeclared option suites to CONFIG["includes"]:
"includes": {
"tls_": OPTIONS_TLS,
"http_": OPTIONS_HTTP,
},
Each include expands into a set of parameters. The dict key ("tls_", "http_") is a prefix prepended to every generated parameter key. In the credential form, suite parameters render after the script’s own fields as collapsed sections named for the suite (TLS, HTTP); a section holding an empty required field is forced open with a Needs attention badge. When the same suite is included under two prefixes, each section is named for its prefix, for example TLS (src_tls).
OPTIONS_TLS
With the conventional tls_ prefix, OPTIONS_TLS generates these keyword arguments:
| Generated kwarg | Type | Default | Description |
|---|---|---|---|
tls_disable_validation |
bool |
False |
Allow connections to endpoints with untrusted TLS certificates. |
tls_ca_cert |
textarea |
— | PEM-encoded certificate authorities to trust when validating the endpoint certificate. |
tls_peer_hash |
string |
— | SHA-256 fingerprint of the endpoint certificate to trust (certificate pinning). Multiple pins can be separated by commas, semicolons, spaces, or newlines. |
tls_client_cert |
textarea |
— | PEM-encoded client certificate to present for mutual TLS. |
tls_client_key |
secret |
— | PEM-encoded private key for the client certificate. Required when tls_client_cert is set. |
OPTIONS_HTTP
With the conventional http_ prefix, OPTIONS_HTTP generates:
| Generated kwarg | Type | Default | Description |
|---|---|---|---|
http_user_agent |
string |
"" |
Optional User-Agent header sent with HTTP requests. |
Using the option suites
Declaring the includes only creates the form fields; the script must pass the collected values to the HTTP client. The kwargs module does this in one call:
load("http", "get_json", "bearer")
load("kwargs", "get_string", "get_url_base", "get_http_options")
def main(*args, **kwargs):
http_options = get_http_options(kwargs, headers={
"Authorization": bearer(get_string(kwargs, "api_token")),
"Accept": "application/json",
})
data, err = get_json("{}/v1/devices".format(get_url_base(kwargs)), **http_options)
get_http_options(kwargs, prefix="http_", tls_prefix="tls_", headers=None) gathers the suite values into a dict of keyword arguments (headers= and tls=) that can be splatted into any http module function (get, post, get_json, post_json, oauth2_token, and so on). The mapping is:
| Suite kwarg | Effect |
|---|---|
tls_disable_validation |
tls={"insecure": True} |
tls_ca_cert |
tls={"ca_pem": ...} |
tls_client_cert / tls_client_key |
tls={"client_cert_pem": ..., "client_key_pem": ...} |
tls_peer_hash |
tls={"thumbprints": [...]} |
http_user_agent |
headers={"User-Agent": ...} (only when the header is not already set) |
Use get_http_tls(kwargs, "tls_") when a script only needs the tls= dict and manages headers itself. The tls= dict can also be built by hand; it accepts the keys insecure, server_name, ca_pem, client_cert_pem, client_key_pem, and thumbprints, and rejects anything else.
Two things to be aware of:
runzero script --validateverifies the wiring: a script that declaresOPTIONS_HTTPorOPTIONS_TLSbut never passes the collected options to an HTTP call fails validation.- The stateful
requests.Sessionobject does not accept thetls=dict; only itsinsecure_skip_verifyconstructor flag is available. Prefer thehttpmodule functions when the TLS suite matters.
Multiple endpoints
Scripts that talk to more than one endpoint can include a suite more than once under different prefixes, and collect each set separately:
"includes": {
"src_tls_": OPTIONS_TLS,
"src_http_": OPTIONS_HTTP,
"dst_tls_": OPTIONS_TLS,
"dst_http_": OPTIONS_HTTP,
},
src_options = get_http_options(kwargs, "src_http_", "src_tls_", src_headers)
dst_options = get_http_options(kwargs, "dst_http_", "dst_tls_", dst_headers)
A complete example
The script below puts the pieces together: a CONFIG block with typed parameters and both option suites, option-suite plumbing via get_http_options, paginated fetching with get_json, and streaming import via report_assets. It passes runzero script --validate as-is, and can be used as a starting point for a real integration (see also the boilerplate template on GitHub).
CONFIG = {
"id": "runzero-example",
"name": "Example integration",
"type": "inbound",
"description": "Imports devices from the Example API.",
"version": "1",
"minVersion": "5.1.0",
"params": [
{"key": "url", "label": "Example API URL", "type": "url", "required": True},
{"key": "api_token", "label": "API token", "type": "secret", "required": True},
],
"includes": {
"tls_": OPTIONS_TLS,
"http_": OPTIONS_HTTP,
},
}
load("runzero.types", "ImportAsset", "to_custom_attributes")
load("net", "network_interface")
load("http", "get_json", "bearer")
load("kwargs", "require", "get_string", "get_url_base", "get_http_options")
def build_asset(device):
return ImportAsset(
id=str(device["id"]),
hostnames=[device.get("hostname", "")],
os=device.get("os", ""),
networkInterfaces=[network_interface(mac=device.get("mac"),
ips=device.get("ips", []))],
customAttributes=to_custom_attributes(device),
)
def main(*args, **kwargs):
require(kwargs, "url", "api_token")
base_url = get_url_base(kwargs)
http_options = get_http_options(kwargs, headers={
"Authorization": bearer(get_string(kwargs, "api_token")),
"Accept": "application/json",
})
total = 0
page = 1
while True:
data, err = get_json("{}/v1/devices".format(base_url),
params={"page": str(page)}, retries=2, **http_options)
if err:
print("request failed: {}".format(err))
break
devices = data or []
if not devices:
break
total += report_assets([build_asset(d) for d in devices])
page += 1
print("imported {} devices".format(total))
return None
runzero script --filename example-integration.star --validate
INFO example-integration.star validated with 1 HTTP request(s)
INFO validated 1 script(s) with dummy HTTP/TLS server https://127.0.0.1:52084
Controlling how assets merge
By default, imported assets merge with existing inventory using the asset id, MAC address, IP address, and hostname. Merge policy is declared once for the whole integration, as the matchBehavior key of the CONFIG block:
CONFIG = {
# ...
"matchBehavior": "no-mac-break no-ip-break no-name-break",
}
matchBehavior accepts a space-separated string of flags built from no- + (id|mac|ip|name) + (-match|-break), plus the break-only no-type-break, which allows merges across an integration’s own asset types. Two presets cover most cases:
"no-mac-break no-ip-break no-name-break"— use when your source supplies a stable, unique id (vendor UUID, serial number). The id still drives merges, but differing MACs, IPs, or names will not disqualify a merge with an existing asset."no-id-match no-id-break"— use when your source only emits ephemeral or per-run ids. The id is ignored and merging falls back to MAC, IP, and hostname.
matchBehavior was formerly a per-asset field on ImportAsset. It no longer is: a script that reads or sets ImportAsset.matchBehavior fails with an error pointing at the CONFIG key.
Two related controls remain per-asset:
assetTypeonImportAssetoverrides the CONFIG-levelassetTypefor one asset. When the CONFIG also declaresassetTypeBehavior, the asset’s type selects which merge policy applies to it.trust_device_type,trust_os, andtrust_os_version(booleans) apply the script’sdeviceType,os, andosVersionvalues to the asset fingerprint even when runZero cannot normalize them through its fingerprint engine. The CONFIG-leveltrustType,trustOS, andtrustOSVersiondo the same for every asset the integration reports.
Leave matchBehavior unset to keep the default matcher behavior, which is correct for most integrations. See the asset identity guidance in the custom integrations repository for a deeper treatment.
Testing scripts with the runZero CLI
The runZero CLI includes a script sub-command for developing and debugging integration scripts locally, using the same Starlark engine and modules as the Explorer.
runzero script [--filename file] [--args a] [--kwargs key=value]
runzero script repl [--filename file]
| Flag | Description |
|---|---|
-f, --filename |
Script file to load and run. --validate also accepts a directory. |
--args |
Positional argument passed to main (repeatable). |
--kwargs key=value |
Keyword argument passed to main (repeatable). |
--validate |
Validate the script CONFIG and smoke-test HTTP/TLS wiring against a local dummy server. |
-o, --output |
Directory to write scan output to (scan.runzero.gz). |
--overwrite |
Replace the output directory if it already exists. |
--custom-integration-id |
Integration UUID stamped on exported records; required with --output. |
--starlark-allow-cidrs |
Restrict every network connection the script makes to the listed CIDRs or IPs (default deny). Enforced after DNS resolution, across the http, requests, socket, direct-protocol, and SQL modules. |
--starlark-block-cidrs |
Block script connections to the listed CIDRs or IPs (default allow). Same coverage as --starlark-allow-cidrs. |
Running scripts
Save a minimal script as hello.star:
def main(*args, **kwargs):
print("Hello {}!".format(kwargs.get("name", "world")))
runzero script --filename hello.star --kwargs name=Dave
INFO script: Hello Dave!
INFO script completed successfully with 0 returned asset(s)
--args and --kwargs can be repeated to pass as many values as needed:
runzero script --filename hello.star --args one --args two --kwargs api_token=foo --kwargs url=https://api.example.com
Note that a local run calls main with exactly the arguments you provide; CONFIG defaults and validation are applied by the console and Explorer at task time, and by --validate locally.
Validating scripts
--validate checks a script end-to-end without touching the real vendor API:
runzero script --filename example.star --validate
INFO example.star validated with 1 HTTP request(s)
INFO validated 1 script(s) with dummy HTTP/TLS server https://127.0.0.1:52084
For HTTP integrations, validation parses the CONFIG block, generates type-appropriate placeholder values for every parameter, initializes the script, calls main, and transparently routes all HTTP requests to a local TLS server that returns canned responses. It fails if the script never makes an HTTP request, and if declared OPTIONS_HTTP/OPTIONS_TLS options never reach the HTTP client.
Scripts with "validationMode": "compile" (templates and direct-protocol integrations) stop after verifying that CONFIG parses and main exists.
Passing a directory validates every .star file under it:
runzero script --filename ./my-integrations/ --validate
A successful validation is a CONFIG and wiring check, not proof that the vendor API accepts your credentials or returns the expected payload — test against the real API with --kwargs for that.
Exporting scan output
To inspect exactly what a script would import, write real scan output to a directory:
runzero script --filename example.star --kwargs api_token=MY_TOKEN -o ./out --overwrite --custom-integration-id 11111111-2222-3333-4444-555555555555
This writes ./out/scan.runzero.gz, a gzip-compressed stream of JSON records — one per asset — in the same format an integration task produces. Without --output, assets streamed via report_assets are discarded with a warning and only counted.
REPL
runzero script repl starts an interactive session with all modules preloaded. With --filename, the script’s functions and globals are available to call directly:
$ runzero script repl --filename hello.star
>>> main(**{"name": "Dave"})
INFO script: Hello Dave!
>>> load('json', json_decode='decode')
>>> data = json_decode('{"greeting": "hello"}')
>>> print(data["greeting"])
INFO script: hello
Exit the REPL with ^D.
Building and testing a script in the console
You do not have to write a script in a local editor and paste it in. The custom integration page in the console is a script workbench: an editor with a test-credentials panel, a live output console, and — where it is enabled — an AI assistant that can draft or revise the script for you. Everything is on the page itself; there is no separate workspace to open or close.
The workbench is laid out as:
- a pinned action bar that stays on screen while the page scrolls. Its left half holds the verdict row — one line stating the result of the last thing you did (validated, ran, saved, loaded, or generated; empty until there is something to report) — and the example picker. Its right half holds Revert, Validate, Run test (which becomes Stop while a run is going), and Full screen.
- the editor, with a rail beside it carrying two tabs: Test credentials (the default) and Generative AI. Switching tabs never interrupts a test run or a generation. The editor has its own resize grabber for height; there are no drag dividers.
- the output console directly under the editor, a capped scroller carrying the detail: test-run logs and reported assets, per-line compile errors, and the progress and transcript of an AI generation.
Saving is the page’s Save (or Update) button, or Cmd/Ctrl+S from anywhere in the workbench. A workbench save reports Script changes saved in the verdict row rather than a toast. On a brand-new integration, Cmd/Ctrl+S creates the integration in place: the URL moves to the edit page without disturbing the editor, a generation, or a running test.
Full screen fills the window with the editor alone. Esc (or the Exit full screen button) leaves it, Cmd/Ctrl+S still saves, and the label notes unsaved changes whenever the editor differs from the last saved script. Full screen is unavailable while a generation is writing into the editor.
While a test run or a generation is in flight, the Enable custom integration script toggle refuses to turn off and says why. Turning the toggle off otherwise stashes the editor content locally; turning it back on restores it.
An empty editor shows a starter panel with three ways in: Generate with AI, Load a blank template, and Browse examples.
Starting from an example
The example picker in the action bar (Select an example…) loads a shipped integration into the editor as a starting point. These are the same scripts published in the runzero-custom-integrations repository, grouped as Start from scratch (a blank template with commented examples), Import assets from a vendor, and Other automations. The library ships through content updates, so it grows and refreshes between releases.
Loading an example replaces what is in the editor, so it asks first when you have something there. On an integration that has already been saved, your previous script is written to version history before it is replaced, so you can always load it back. Loading also fills in an empty integration name and description from the example, and picking the example already in the editor reports No change.
The integration remembers which example a script came from and at which version. When the shipped copy no longer matches yours, a banner says A newer version of the name example is available, notes when you have local modifications, and offers Update to v_N_. Updating also asks first, and also snapshots what you have.
Generating a script with AI
Where generative AI is enabled for your account, the workbench rail has a Generative AI tab (an empty editor also offers Generate with AI directly). The tab holds a short form:
- What should this integration do? — describe the vendor, the API, and the data you want imported, in your own words. (When revising an existing script, the field asks What should change?) The Authentication, Asset fields and Scope buttons under the box insert the three questions the assistant most needs answered and cannot guess: how the API authenticates, which response fields are the address, MAC and hostname, and what to leave out.
- Documentation links (optional) — one vendor API documentation URL per line, up to 10. The field says how many of what you entered will actually be read.
Press Generate to write a new script, or Update script to revise the one already in the editor. The assistant works for several minutes: it consults the shipped integration library for the same vendor, reads the documentation you linked, and writes the script. Before it answers, the finished script must pass runZero’s real validators — a script that fails is sent back to the assistant to fix, shown as Fixing validation errors — and any load() lines the script forgot are added mechanically afterward.
Progress appears in the output console below the editor — the pages it reads, the tools it calls, and its own commentary — and the script itself streams into the editor as it is written. You can switch back to the Test credentials tab while it works; the run keeps going and keeps reporting in the verdict row.
Stop — in the AI form, or beside the transcript in the output console — cancels the run. Nothing else does: an AI run is deliberately independent of the browser, so leaving the page or closing the tab leaves it running. Returning to the integration page picks the run back up, including anything that streamed while you were away. Every attempt also runs as a live turn on its own AI thread, with the running indicator, the work log, and its own Stop, so a run that failed or that you left behind is still there to look at — the output console links it as View this request in AI Threads.
If a run fails or is stopped before it finishes, whatever script was in the editor beforehand is put back. A run can also finish without a script — the assistant declining, asking a question, or answering in prose — and that answer is shown under What the model said.
When it succeeds, the assistant’s own account of what it built stays in the output console under What the model says it built, beside the code. Clear removes the transcript when you are done with it.
What the assistant is given, and what it is not
Only three things are sent to the model: your description, the documentation URLs you supplied, and — when you are revising — the script currently in the editor. No inventory, asset, organization or credential data is sent, ever.
The assistant has four tools: it can fetch a documentation page, list and read runZero’s shipped example integrations, and validate a candidate script. Validation parses and compiles the script only — nothing is executed, no network call is made, and nothing is saved.
Documentation pages are fetched under the same protections as the rest of the platform: private and internal addresses are blocked, and the content is treated as untrusted throughout, because a page the assistant reads is written by someone else.
Read the script before you save or run it. It is drafted from third-party documentation and it will run with your integration credentials.
Limits
- One authoring run is bounded at 10 minutes.
- Documentation fetching within a run is bounded at 10 pages and 90 seconds in total.
- A script sent for revision must be under 48 KiB. Above that the run still works, but it is generated from your description alone, and the form says so.
- Generative AI requires the AI feature to be enabled for the account and the organization, in addition to the custom integration script entitlement, and administrator access — the same requirement as authoring a script by hand.
- Generations count against the same AI run limits as threads: 4 running AI jobs per user and 12 per account.
Where the generated script goes
When the run finishes, the script is loaded into the editor and, if the integration has already been saved, committed straight away as a new version — the output console notes Saved as version N — so a generation is never one navigation away from being lost. A script that does not compile is not saved as a version; the output console says so and leaves it in the editor for you to fix. On an integration that has not been saved yet, it reminds you to save the integration to keep the script.
Each run also produces an AI artifact holding the script. Artifacts start private to you and can be shared; an unshared artifact is retained as long as the thread it came from. Artifacts are listed under Artifacts in the AI section of the console, where Use in custom integration loads one back into the integration it was written for (or into a new one). The handoff lands directly on the workbench: the editor flashes, and the verdict row confirms what was loaded — for a new integration, with a suggested name derived from the artifact’s title that you can change before saving.
Validating and previewing
Validate runs the same checks the save path runs: the size cap, the Starlark parse and compile, the CONFIG block extraction and schema, and the reserved-name rules. Nothing is executed. Failures are reported per line in the output console, and the editor marks the line.
The Credential fields disclosure below the workbench lists every parameter the CONFIG block declares — label, key, type, whether it is required and secret, and its default — refreshed automatically when the integration loads and after each save. It is the quickest way to check that a CONFIG block says what you meant. To see the rendered form itself, switch the test rail to Enter values for this run only, which shows the same fields the person creating the credential will see.
Running a test
Run test executes the script in the editor immediately — including changes you have not saved — and streams back its output, a preview of the assets it reported, and a plain-language verdict.
A test run is not a task, and differs from one in ways that matter:
- It runs on the console, not on an Explorer. Internal and link-local addresses are blocked — on self-hosted consoles too — so an integration that targets an endpoint inside your network cannot be tested this way. Test that one from an Explorer with the runZero CLI instead.
- Nothing is imported. No task is created, no assets reach your inventory, and nothing is written to your organization. The assets shown are a preview and are discarded when the run ends.
- Credentials are not stored. You can run against a saved credential, or switch the rail to Enter values for this run only. Values typed in for a run are held for the length of that run and never saved; to schedule an integration whose CONFIG requires values, you still need a saved credential.
Picking a saved credential shows its stored values in the rail, so the choice is not blind: non-secret values appear verbatim, and secret values show only that a value is stored — they are never sent to the browser. Your non-secret selections (credential mode, credential choice, non-secret parameter values) are remembered per integration in the browser between visits; secret values start empty every time.
Before anything executes, the run checks the credential values against the script’s CONFIG schema; values that can never satisfy it are refused up front, naming the parameter keys (never the values). The rail also warns beside Run test when no credential is selected and the CONFIG declares required fields.
Every credential value supplied to a run — saved or typed in — plus every CONFIG secret value is redacted from everything the run prints, including error text, crash output, and the asset preview.
Test run limits
| Limit | Value |
|---|---|
| Wall clock | 120 seconds |
| Memory | 256 MiB |
| Computation | 2,000,000,000 interpreter steps; time spent waiting on an API does not count |
| Assets | 100, after which the run stops and reports success |
| Assets shown in full | the first 10, with up to 24 attributes each, values truncated at 256 bytes |
| Log output | 500 lines, 4 KiB per line |
You can run one test at a time, up to six runs a minute; the account as a whole is capped at twenty starts a minute.
Reaching the 100-asset ceiling is reported as a success: it means the script worked and was stopped early because it is a test. A real task is not capped this way.
Every run ends with a named outcome rather than a bare failure — it completed, it stopped at the asset limit, you stopped it, the script raised an error, it ran out of time, memory, computation or stack, the sandbox stopped it, it stopped unexpectedly, or it never started — each with a short explanation of what to look at next. A run that completes without reporting any assets says so: it usually means the response was parsed differently than expected, or a filter in the script excluded everything.
A run that could not be started at all is explained the same way, including the compiler’s own line and column when the script did not compile, so you do not have to press Validate to find out what was wrong.
Version history
Every change to a script is snapshotted, including versions committed by an AI generation. The Script version history disclosure on the custom integration page — its title carries the count and the current version — opens a table listing each version with its author, timestamp, size, and the example it came from, when it came from one. The most recent 25 versions are kept.
Load, offered on every version other than the current one, puts a stored version into the editor so you can look at it or work from it. It asks first when the editor holds something else. Nothing is written until you save, and saving it creates a new version rather than rewriting history.
Revert appears in the action bar whenever the editor differs from the last saved script, and restores that saved script locally. Like Load, it only changes the editor; it asks first, because the editor may be the only copy of what you have typed.
Adding the integration to your console
Step 1: Create the custom integration
- Go to the Custom sources page and click Add custom integration.
- Provide a name (no spaces) and optionally an icon (PNG, up to 256x256).
- Choose whether the integration is available to every organization in the account or only to specific organizations. See organization access.
- Toggle Enable custom integration script, then paste in your script, start from an example, or generate one with AI.
- Click Validate to check the script syntax and CONFIG block, and optionally run the script against a real endpoint before you schedule it.
- Click Save — or Save & create task, which saves and lands on the task form with the integration pre-selected.
Step 2: Create the credential
- Go to the Credentials page and click Add Credential.
- Choose Custom Integration Script Secrets as the credential type.
- Select the custom integration. The form shows the fields declared in the script’s
CONFIGblock, including any option-suite fields such astls_disable_validationandhttp_user_agent. - For legacy scripts without a CONFIG block, provide the Access key (legacy) and Access secret (legacy) values, which are passed to the script as the
access_keyandaccess_secretkwargs. - To let other organizations use this credential, select the Make this a global credential option.
- Save the credential.
Step 3: Create the task
- Go to the custom integration task page, click Integrate on the Tasks page and choose Custom Scripts, or use one of the shortcuts that pre-select the integration: Save & create task on the integration page, or the play action on its row of the Custom sources grid.
- Select the custom integration and the credential created above. Saving the task validates the pair: the script must compile, and the credential must satisfy every parameter the CONFIG block requires — a save with no credential, or a credential missing required parameters, fails with an error naming the parameter keys. (Legacy scripts without a CONFIG block skip this check.)
- Select the Explorer to run the script from, or leave the selection empty to run the task on the console itself. Console-hosted tasks cannot reach private or link-local addresses — and on runZero-hosted consoles they run under a tighter budget — so use an Explorer for anything that targets your internal network.
- Set the site, description, and schedule as appropriate.
- Activate the connection to start the task.
Once the task completes, assets appear in your inventory and can be found with the search custom_integration:<name>. Tasks list the integration with its own icon, and the task search custom_integration: accepts a name or a UUID — including the UUID of a deleted integration.
Organization access
A custom integration is either available to every organization in the account, or available only to the organizations it has been granted to. Custom integrations that existed before this option was added are available to every organization, so their tasks keep working until an administrator narrows the scope of an integration.
Users must have administrator-level permissions to manage custom integrations. Users with Administrator as their default role can manage every custom integration. Users with per-organization permissions only see the integrations available to their organizations, and can only edit an integration if they have administrator permissions in every organization it is available to. An integration that is available to every organization can only be edited by a superuser or a user whose default role is Administrator.
Reading an integration’s script and its configuration parameters requires administrator permissions in one of the organizations the integration has been granted to; the script of an integration available to every organization is visible only to superusers and users whose default role is Administrator. Users, viewers and annotators can see that an integration exists, including its name and icon, so that the asset data it collected stays readable, but they cannot see its script or its configuration parameters. Selecting an integration when configuring a task requires at least user-level (write) access in an organization the integration is available to; viewers and annotators cannot.
An integration task can only use a custom integration that is available to the organization the task belongs to. Access is checked again each time the task runs, so removing an organization’s access also stops tasks that were already scheduled, with an error naming the integration and organization. Deleting an organization removes it from every integration’s access list; an integration that was scoped only to that organization is kept, but is visible only to superusers and account-wide administrators until it is re-scoped.
The same organization scope is exposed on the account API: custom integration objects carry global and organizationIds fields (mutually exclusive), and organization-scoped API tokens only see integrations shared with their organizations.
Deleting a custom integration
Deleting a custom integration requires typing the word delete to confirm, and the confirmation states the impact: the saved script versions are deleted, the data the integration added to assets is removed, and scheduled tasks that use it are paused rather than deleted — each shows the reason, and can be re-pointed at another integration. Credentials are kept and can be used by another integration.
Script limits and sandbox behavior
- Scripts are limited to 1 MiB of source. A script sent to the AI assistant for revision has a much smaller ceiling of 48 KiB.
- Scripts can only
load()the registered modules; there is no filesystem access or relative import. - A script run is bounded by an execution-step ceiling and a wall clock, and the deadline also bounds HTTP requests and
time.sleepcalls. A task running on an Explorer or a self-hosted console gets 24 hours and 720 billion steps; one running on a runZero-hosted console gets 2 hours and 240 billion steps. A console test run is much tighter again, at 120 seconds. Steps are consumed by computation only, so time spent waiting on an API does not count against the ceiling. Self-hosted operators can override both budgets with theRUNZERO_CUSTOM_INTEGRATION_MAX_STEPSandRUNZERO_CUSTOM_INTEGRATION_MAX_RUN_SECONDSenvironment variables. - HTTP response bodies are capped at 1 GiB, and a script’s cumulative network reads — across
http,requests,socket, SSH, and SMB — are capped at 4 GiB per run. Other per-call caps include 64 MiB per SMB file read, 16 MiB per SSH command output stream, and gzip decompression bounded at 1 GiB and a 500:1 expansion ratio (see the library reference). - A script can hold at most 256 open connections and sessions (sockets, SSH, SMB, WinRM, WMI, SQL) at once; closing one frees its slot.
- Values of
secretparameters are automatically redacted fromprintoutput, progress messages, and error text. - On an Explorer task, every network module —
http,requests,socket, and the direct-protocol modules (runzero.ssh,runzero.smb,runzero.winrm,runzero.wmi,runzero.sql) — can reach the internal addresses visible to that Explorer. On console-hosted runs (both test runs and tasks run without an Explorer), private and link-local addresses are blocked for every module. The CLI’s--starlark-allow-cidrsand--starlark-block-cidrsflags pin script egress for local runs.
Available libraries
Load only what you use; each module is available via load(...). Full signatures and runnable examples for every module are on the Starlark libraries page.
| Module | Provides |
|---|---|
runzero.types |
ImportAsset, NetworkInterface, Service, ServiceProtocolData, Software, Vulnerability, to_custom_attributes |
kwargs |
Typed accessors: require, get_string, get_bool, get_int, get_float, get_list, get_url_base, get_http_tls, get_http_options |
coerce |
Total (never-raising) conversions for messy API data: as_text, as_dict, as_list, dicts, as_int, as_float, as_bool, dedupe |
http |
HTTP verbs, get_json/post_json with retries, bearer/basic/oauth2_token, url_encode/url_parse/url_join, multipart |
requests |
Stateful HTTP Session with sticky headers and cookies |
net |
ip_address, network_interface, normalize_mac, ip_network, ip_in_network, resolve, plus identity screens routable_ip, routable_ips, clean_hostname, clean_hostnames, mac_key |
json |
encode, decode, encode_indent, indent |
jsonstream |
iter_array, iter_lines for large JSON/NDJSON responses |
csv |
read_all, read_rows, write_all, write_dicts |
xml |
parse into an element tree |
re |
RE2 regular expressions: match, find_all, sub, split, compile |
time |
now, parse_time, parse_ts, parse_duration, from_timestamp, sleep |
uuid |
new_uuid |
base64 / hex / base32 |
Standard encodings, including raw and URL-safe variants |
crypto |
Hashes, HMAC, AWS SigV4 signing, CSPRNG output |
jwt |
encode, decode, decode_unverified |
gzip |
compress, decompress |
flatten_json |
flatten nested structures |
runzero.progress |
report, info, warn task progress in the console |
socket |
Raw TCP/UDP/TLS connections |
runzero.ssh / runzero.smb / runzero.winrm / runzero.wmi / runzero.sql |
Direct-protocol collection from systems without a REST API |
The predeclared names report_assets, report_asset, pager, max_pages, OPTIONS_TLS, and OPTIONS_HTTP are always available without a load().
Existing Custom Integrations
| Name | Setup Instructions | Integration Code |
|---|---|---|
| Absolute Secure Endpoint | Link | Link |
| AdGuard Home | Link | Link |
| Akamai Guardicore Centra | Link | Link |
| Armis Centrix | Link | Link |
| Asimily | Link | Link |
| Audit Log to Webhook | Link | Link |
| Automox | Link | Link |
| BMC Helix Discovery | Link | Link |
| Bitdefender GravityZone | Link | Link |
| Bitsight | Link | Link |
| Carbon Black | Link | Link |
| Checkmk Raw Edition | Link | Link |
| Cisco Cyber Vision | Link | Link |
| Cisco ISE | Link | Link |
| Cisco Secure Endpoint | Link | Link |
| Claroty CTD | Link | Link |
| Claroty xDome | Link | Link |
| Cortex XDR | Link | Link |
| CyberArk EPM | Link | Link |
| Cybereason | Link | Link |
| Cyberint | Link | Link |
| Cyberwatch | Link | Link |
| Device42 | Link | Link |
| Digital Ocean | Link | Link |
| Drata | Link | Link |
| EfficientIP SOLIDserver | Link | Link |
| ExtraHop Reveal(x) | Link | Link |
| Extreme Networks CloudIQ | Link | Link |
| Fleet (osquery) | Link | Link |
| Foreman | Link | Link |
| Forescout CounterACT | Link | Link |
| Forescout Risk and Exposure Management | Link | Link |
| Forescout eyeInspect | Link | Link |
| Frontline VM | Link | Link |
| GLPI | Link | Link |
| Ghost Security | Link | Link |
| Greenbone (GMP) Import | Link | Link |
| Greenbone (GMP) Scan Launch | Link | Link |
| HCL BigFix | Link | Link |
| HPE Aruba ClearPass | Link | Link |
| Halcyon | Link | Link |
| Home Assistant | Link | Link |
| Icinga 2 | Link | Link |
| Illumio Core | Link | Link |
| Infoblox NIOS | Link | Link |
| Ivanti Neurons | Link | Link |
| JAMF | Link | Link |
| JumpCloud | Link | Link |
| Kandji | Link | Link |
| Kenna Security | Link | Link |
| Kubernetes | Link | Link |
| Lansweeper | Link | Link |
| LibreNMS | Link | Link |
| LimaCharlie | Link | Link |
| Linux via SSH | Link | Link |
| ManageEngine Endpoint Central | Link | Link |
| Maze | Link | Link |
| Microsoft Defender for IoT | Link | Link |
| Microsoft SQL Server databases | Link | Link |
| Microsoft WSUS | Link | Link |
| MikroTik RouterOS | Link | Link |
| Miradore | Link | Link |
| Mosyle | Link | Link |
| Nautobot | Link | Link |
| Netdata | Link | Link |
| Netdisco | Link | Link |
| Netskope | Link | Link |
| Nexthink | Link | Link |
| NinjaOne | Link | Link |
| Nozomi Networks | Link | Link |
| Nutanix Prism | Link | Link |
| OCS Inventory NG | Link | Link |
| OPNsense | Link | Link |
| Open-AudIT Community | Link | Link |
| OpenNMS Horizon | Link | Link |
| OpenWrt | Link | Link |
| Palo Alto Networks Device Security | Link | Link |
| Pi-hole | Link | Link |
| Portainer / Docker Engine | Link | Link |
| Proxmox | Link | Link |
| PuppetDB | Link | Link |
| Quest KACE SMA | Link | Link |
| Red Hat Insights | Link | Link |
| SAP LeanIX | Link | Link |
| Scale Computing | Link | Link |
| Scan Passive Assets | Link | Link |
| Slurp'it | Link | Link |
| Snipe-IT | Link | Link |
| Snow License Manager | Link | Link |
| SolarWinds Information Service | Link | Link |
| Sophos Central | Link | Link |
| Stairwell | Link | Link |
| Sumo Logic | Link | Link |
| Synology DSM | Link | Link |
| TP-Link Omada | Link | Link |
| Tactical RMM | Link | Link |
| Tailscale | Link | Link |
| Tenable OT Security | Link | Link |
| Trellix ePolicy Orchestrator | Link | Link |
| Trend Micro Vision One | Link | Link |
| TrueNAS | Link | Link |
| Ubiquiti UniFi Network | Link | Link |
| Ubiquiti UniFi Protect | Link | Link |
| Ubiquiti UniFi Site Manager | Link | Link |
| Unraid | Link | Link |
| Uptycs | Link | Link |
| Vulnerability Workflow | Link | Link |
| Wazuh | Link | Link |
| Windows SMB shares | Link | Link |
| Windows WMI | Link | Link |
| Workspace ONE UEM | Link | Link |
| Zabbix | Link | Link |
| exe.dev | Link | Link |
| iTop | Link | Link |
| ntopng | Link | Link |
| pfSense | Link | Link |
| phpIPAM | Link | Link |
| runZero Task Sync | Link | Link |