Custom Integration Scripts

View as Markdown

Community Platform

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 CONFIG block 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_TLS and OPTIONS_HTTP) add standard connection controls to any script without copying boilerplate parameters.
  • report_assets streams assets to runZero page-by-page, so large imports no longer need to be held in memory.
  • A CONFIG-level matchBehavior policy and per-asset trust_* flags give scripts control over how imported assets merge with existing inventory.
  • A much larger library of built-in modules, including typed kwargs accessors, 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 script CLI command gained a --validate mode that smoke-tests a script’s CONFIG and HTTP/TLS wiring against a local dummy server, and an --output mode 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:

  1. Write the script, starting with a CONFIG block and a main function.
  2. Test it locally with the runZero CLI (runzero script).
  3. Create the custom integration in the console and paste in the script.
  4. Create a credential; the form is generated from the script’s CONFIG parameters.
  5. 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:

  1. There is no exception handling (try/except). Use return values to signal errors.
  2. There is no f-string f'{var}' formatting. Use "{}".format(var) for string interpolation.
  3. 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]
  • args receives any positional arguments configured on the task, as strings.
  • kwargs receives the credential fields and task keyword arguments, as strings. Declare every key your script reads in CONFIG["params"], and read them with the typed kwargs accessors.
  • For inbound integrations, main returns a list of ImportAsset objects (or a single ImportAsset). Returning None is valid when assets are streamed with report_assets instead.
  • For outbound integrations, main performs its export work and returns None.

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 apply default values before main runs.
  • Route secret parameters 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:

  • CONFIG must 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 is CONFIG["includes"], which may reference the predeclared option-set identifiers OPTIONS_TLS and OPTIONS_HTTP.
  • Scripts without a CONFIG block continue to run with the legacy access_key and access_secret credential fields, so existing integrations keep working unchanged.
  • sourceId and sourceName are 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 kwargs under its key. Values are delivered as strings; use the kwargs module (get_string, get_int, get_bool, get_list, …) to read them with type coercion and defaults.
  • Declared default values are applied, enum aliases are normalized, and validation runs before main is 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_key and access_secret fields 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 --validate verifies the wiring: a script that declares OPTIONS_HTTP or OPTIONS_TLS but never passes the collected options to an HTTP call fails validation.
  • The stateful requests.Session object does not accept the tls= dict; only its insecure_skip_verify constructor flag is available. Prefer the http module 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:

  • assetType on ImportAsset overrides the CONFIG-level assetType for one asset. When the CONFIG also declares assetTypeBehavior, the asset’s type selects which merge policy applies to it.
  • trust_device_type, trust_os, and trust_os_version (booleans) apply the script’s deviceType, os, and osVersion values to the asset fingerprint even when runZero cannot normalize them through its fingerprint engine. The CONFIG-level trustType, trustOS, and trustOSVersion do 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

  1. Go to the Custom sources page and click Add custom integration.
  2. Provide a name (no spaces) and optionally an icon (PNG, up to 256x256).
  3. Choose whether the integration is available to every organization in the account or only to specific organizations. See organization access.
  4. Toggle Enable custom integration script, then paste in your script, start from an example, or generate one with AI.
  5. Click Validate to check the script syntax and CONFIG block, and optionally run the script against a real endpoint before you schedule it.
  6. Click Save — or Save & create task, which saves and lands on the task form with the integration pre-selected.

Step 2: Create the credential

  1. Go to the Credentials page and click Add Credential.
  2. Choose Custom Integration Script Secrets as the credential type.
  3. Select the custom integration. The form shows the fields declared in the script’s CONFIG block, including any option-suite fields such as tls_disable_validation and http_user_agent.
  4. 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_key and access_secret kwargs.
  5. To let other organizations use this credential, select the Make this a global credential option.
  6. Save the credential.

Step 3: Create the task

  1. 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.
  2. 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.)
  3. 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.
  4. Set the site, description, and schedule as appropriate.
  5. 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.sleep calls. 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 the RUNZERO_CUSTOM_INTEGRATION_MAX_STEPS and RUNZERO_CUSTOM_INTEGRATION_MAX_RUN_SECONDS environment 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 secret parameters are automatically redacted from print output, 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-cidrs and --starlark-block-cidrs flags 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

NameSetup InstructionsIntegration Code
Absolute Secure EndpointLinkLink
AdGuard HomeLinkLink
Akamai Guardicore CentraLinkLink
Armis CentrixLinkLink
AsimilyLinkLink
Audit Log to WebhookLinkLink
AutomoxLinkLink
BMC Helix DiscoveryLinkLink
Bitdefender GravityZoneLinkLink
BitsightLinkLink
Carbon BlackLinkLink
Checkmk Raw EditionLinkLink
Cisco Cyber VisionLinkLink
Cisco ISELinkLink
Cisco Secure EndpointLinkLink
Claroty CTDLinkLink
Claroty xDomeLinkLink
Cortex XDRLinkLink
CyberArk EPMLinkLink
CybereasonLinkLink
CyberintLinkLink
CyberwatchLinkLink
Device42LinkLink
Digital OceanLinkLink
DrataLinkLink
EfficientIP SOLIDserverLinkLink
ExtraHop Reveal(x)LinkLink
Extreme Networks CloudIQLinkLink
Fleet (osquery)LinkLink
ForemanLinkLink
Forescout CounterACTLinkLink
Forescout Risk and Exposure ManagementLinkLink
Forescout eyeInspectLinkLink
Frontline VMLinkLink
GLPILinkLink
Ghost SecurityLinkLink
Greenbone (GMP) ImportLinkLink
Greenbone (GMP) Scan LaunchLinkLink
HCL BigFixLinkLink
HPE Aruba ClearPassLinkLink
HalcyonLinkLink
Home AssistantLinkLink
Icinga 2LinkLink
Illumio CoreLinkLink
Infoblox NIOSLinkLink
Ivanti NeuronsLinkLink
JAMFLinkLink
JumpCloudLinkLink
KandjiLinkLink
Kenna SecurityLinkLink
KubernetesLinkLink
LansweeperLinkLink
LibreNMSLinkLink
LimaCharlieLinkLink
Linux via SSHLinkLink
ManageEngine Endpoint CentralLinkLink
MazeLinkLink
Microsoft Defender for IoTLinkLink
Microsoft SQL Server databasesLinkLink
Microsoft WSUSLinkLink
MikroTik RouterOSLinkLink
MiradoreLinkLink
MosyleLinkLink
NautobotLinkLink
NetdataLinkLink
NetdiscoLinkLink
NetskopeLinkLink
NexthinkLinkLink
NinjaOneLinkLink
Nozomi NetworksLinkLink
Nutanix PrismLinkLink
OCS Inventory NGLinkLink
OPNsenseLinkLink
Open-AudIT CommunityLinkLink
OpenNMS HorizonLinkLink
OpenWrtLinkLink
Palo Alto Networks Device SecurityLinkLink
Pi-holeLinkLink
Portainer / Docker EngineLinkLink
ProxmoxLinkLink
PuppetDBLinkLink
Quest KACE SMALinkLink
Red Hat InsightsLinkLink
SAP LeanIXLinkLink
Scale ComputingLinkLink
Scan Passive AssetsLinkLink
Slurp'itLinkLink
Snipe-ITLinkLink
Snow License ManagerLinkLink
SolarWinds Information ServiceLinkLink
Sophos CentralLinkLink
StairwellLinkLink
Sumo LogicLinkLink
Synology DSMLinkLink
TP-Link OmadaLinkLink
Tactical RMMLinkLink
TailscaleLinkLink
Tenable OT SecurityLinkLink
Trellix ePolicy OrchestratorLinkLink
Trend Micro Vision OneLinkLink
TrueNASLinkLink
Ubiquiti UniFi NetworkLinkLink
Ubiquiti UniFi ProtectLinkLink
Ubiquiti UniFi Site ManagerLinkLink
UnraidLinkLink
UptycsLinkLink
Vulnerability WorkflowLinkLink
WazuhLinkLink
Windows SMB sharesLinkLink
Windows WMILinkLink
Workspace ONE UEMLinkLink
ZabbixLinkLink
exe.devLinkLink
iTopLinkLink
ntopngLinkLink
pfSenseLinkLink
phpIPAMLinkLink
runZero Task SyncLinkLink
Updated