Learn how to create custom Invoker scripts to extend SIERRA's functionality and automate your investigation workflows.
Invoker scripts let you run any external tool or script — in any language — directly from the SIERRA canvas. Define them in simple YAML, then run them from your investigation graph to automate tasks, call APIs, and capture results back into the graph for immediate analysis.
Copy a ready-made Markdown prompt for ChatGPT, Claude, or any other model. It includes the valid SIERRA schema, input behavior for files, images, and collection snapshots, V1/V2 output rules, and the response format we want back.
After pasting it into the model, replace the finalUser Requestsection with your workflow.
An Invoker script is defined by a YAML configuration file, which tells SIERRA how to present parameters, run your command, and handle its output in the graph. UseType: IMAGEwhen your tool expects a picture on disk, orType: COLLECTIONwhen it needs a whole group of SIERRA nodes and edges. It has the following structure:
STRINGText from the user or graph
Use for domains, usernames, IDs, API keys, prompts, and short investigation values.
FILEA selected local file path
Use when your script expects a normal path to a user-selected file on disk.
IMAGEA resolved image file path
Use when the input may start as a pasted image, local image, or remote image URL, but your script wants a file path.
COLLECTIONA graph snapshot
Use when your script needs a whole collection: descendant nodes, nested collections, and internal edges.
yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20# [Optional]: Working directories SIERRA will try as cwd when executing Command # These entries are not executable search roots PATHS: - /path/to/script/directory - /another/path # [Mandatory]: Contains the list of Invoker scripts SCRIPTS: - Name: Unique Script Name # [Mandatory] Description: Brief description of the script Protocol: V2 # [Optional] omit for V1 batch invokers Params: # [Mandatory] - Name: ParameterName # [Mandatory] Description: Parameter description # [Optional] # [Mandatory]: The data type of the parameter # User-facing types: STRING, FILE, IMAGE, COLLECTION Type: STRING Options: # [Optional] - MANDATORY # indicates this parameter cannot be empty Command: command_to_execute {ParameterName} # [Mandatory]
IMAGEis resolved to a real image file path before command substitution. Users can supply a pasted clipboard image, a local image path or chosen file, or a remote image URL. Your script should read the substituted value exactly like any other image file path on disk.yaml
1 2 3 4 5 6 7 8 9 10SCRIPTS: - Name: OCR Screenshot Description: Read text from an image Params: - Name: Image Description: Clipboard image, local file, or remote image URL Type: IMAGE Options: - MANDATORY Command: python ocr_image.py "{Image}"
python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21import json import sys from pathlib import Path image_path = Path(sys.argv[1]) if not image_path.is_file(): print(json.dumps({ "type": "Error", "message": "Image file was not found", })) raise SystemExit(0) image_bytes = image_path.read_bytes() print(json.dumps({ "type": "Tree", "results": [ f"# OCR Image\n{image_path.name}", f"Loaded {len(image_bytes)} bytes from {image_path}", ], }))
COLLECTION sends a JSON snapshot for one collection node: descendants, nested collections, and internal edges.
COLLECTION parameter, or start from a collection-node invoker suggestion.@{Collection_JSON_FILE} so Python reads a JSON file. Replace Collection with your param name, such as Evidence.A collection input has four top-level fields: kind, title, nodes, and edges. Use node IDs and edges for analysis; treat layout fields like position as optional visual context.
id, type, position, optional parentId, and data. Text content is usually at data.content.type: collectionEntity. Their child nodes point back with parentId.source and target node IDs. Labels usually live at edge.data.text.json
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33{ "kind": "collection", "title": "Case Evidence", "nodes": [ { "id": "domain-node", "type": "entity", "position": { "x": 0, "y": 0 }, "data": { "content": "example.com" } }, { "id": "child-collection", "type": "collectionEntity", "position": { "x": 220, "y": 0 }, "data": { "title": "Login hosts" } }, { "id": "login-node", "type": "entity", "parentId": "child-collection", "position": { "x": 24, "y": 72 }, "data": { "content": "https://login.example.com" } } ], "edges": [ { "id": "edge-1", "source": "domain-node", "target": "login-node", "data": { "text": "expands to" } } ] }
yaml
1 2 3 4 5 6 7 8 9 10SCRIPTS: - Name: Collection Brief Description: Summarize a selected collection's nodes and relationships Params: - Name: Collection Description: Collection node to summarize Type: COLLECTION Options: - MANDATORY Command: python summarize_collection.py "@{Collection_JSON_FILE}"
python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52import json import sys from pathlib import Path def load_collection(raw_value): raw_value = raw_value.strip() if raw_value.startswith("@"): return json.loads(Path(raw_value[1:]).read_text(encoding="utf-8")) return json.loads(raw_value) try: collection = load_collection(sys.argv[1]) except Exception as exc: print(json.dumps({ "type": "Error", "message": f"Could not read collection input: {exc}", })) raise SystemExit(0) if collection.get("kind") != "collection": print(json.dumps({ "type": "Error", "message": "Expected a COLLECTION input from SIERRA.", })) raise SystemExit(0) nodes = collection.get("nodes", []) edges = collection.get("edges", []) node_by_id = {node.get("id"): node for node in nodes} def node_label(node): data = node.get("data") or {} return data.get("title") or data.get("content") or node.get("id") or "Untitled node" relationships = [] for edge in edges[:8]: source = node_label(node_by_id.get(edge.get("source"), {})) target = node_label(node_by_id.get(edge.get("target"), {})) label = (edge.get("data") or {}).get("text") or "connected to" relationships.append(f"- {source} -- {label} --> {target}") print(json.dumps({ "type": "Tree", "results": [ f"# Collection Brief: {collection.get('title', 'Collection')}", f"Nodes: {len(nodes)}", f"Edges: {len(edges)}", { "Relationships": relationships or ["No internal edges found."] } ], }))
PATHSonly controls the working directory SIERRA uses while running your command. It does not tell SIERRA where to discover executables. Use full commands or rely on the runtime PATH available in that working directory.SIERRA currently supports two invoker output contracts:V1 for batch/final JSON, and V2 for incremental node emission.
V2support when talking to SIERRA Cloud, so cloud-only streaming invokers can be hidden from legacy clients.V1: The command finishes first, then stdout is parsed once as a finalTree,Network, orErrorobject.
V2: The command writes one JSON object per line to stdout while it runs.resultevents create nodes immediately,progressupdates the invoker UI, andendcloses the stream.
For the default V1 contract, your script should return JSON in one of these supported formats:
The type field should be Tree. Perfect for hierarchical data structures.
json
1 2 3 4 5 6 7 8 9 10 11 12 13{ "type": "Tree", "results": [ "Content of Entity A", "Content of Entity B", { "Content of Entity C (parent of D and E)": [ "Content of Entity D", "Content of Entity E" ] } ] }
The type field should be Network. Ideal for complex relationship mapping.
json
1 2 3 4 5 6 7 8 9 10 11 12 13{ "type": "Network", "origins": ["AliceID"], "nodes": [ { "id": "AliceID", "content": "Alice" }, { "id": "BobID", "content": "Bob" }, { "id": "CharlieID", "content": "Charlie" } ], "edges": [ { "source": "AliceID", "target": "BobID", "label": "friend" }, { "source": "AliceID", "target": "CharlieID", "label": "colleague" } ] }
When your script encounters an error, return a JSON object with type: "Error".
json
1 2 3 4 5 6 7 8 9 10 11// Service error { "type": "Error", "message": "API connection failed" } // Input validation error { "type": "Error", "message": "Invalid email format" }
Use Protocol: V2 when your invoker benefits from incremental results, such as search, scraping, enumeration, or long-running collection jobs.
yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14PATHS: - /opt/scripts SCRIPTS: - Name: Username Search Description: Emits usernames as they are found Protocol: V2 Params: - Name: Username Description: Username to search Type: STRING Options: - MANDATORY Command: python username_search.py "{Username}"
Your script should write one compact JSON object per line tostdout. Do not pretty-print multi-line JSON.
python
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33import json def emit(event): print(json.dumps(event), flush=True) username = "alice" emit({ "version": 2, "type": "progress", "message": f"Searching for {username}" }) emit({ "version": 2, "type": "result", "id": "root", "content": f"# Username Search\n{username}" }) emit({ "version": 2, "type": "result", "id": "github", "parent": "root", "content": "# GitHub\nhttps://github.com/alice" }) emit({ "version": 2, "type": "end", "summary": "1 account found" })
Recommended event types: result,progress,end, anderror.
Parenting: omit parent to attach directly to the invoker node, or reference a previously emitted result id.
Logging: keep protocol events on stdout; send ordinary debug logs to stderr.
Here's a complete example of an Invoker script configuration for a subdomain lookup utility:
yaml
1 2 3 4 5 6 7 8 9 10 11 12 13 14PATHS: - /opt/scripts - /home/user/tools SCRIPTS: - Name: Subdomain Finder Description: Looks up subdomains of a given domain using crt.sh Params: - Name: Domain Description: The domain to find subdomains for Type: STRING Options: - MANDATORY Command: python subfinder.py "{Domain}"
Follow these guidelines for optimal Invoker script development:
•
Validate inputs: Always validate parameters before processing
•
Handle errors gracefully: Return meaningful error messages
•
Use descriptive names: Make script and parameter names self-explanatory
•
Test thoroughly: Verify output formats work correctly in SIERRA
•
Document your scripts: Include helpful descriptions and examples
Start building Invokers to supercharge your investigations, from quick lookups to complex multi-step automations.
In silence, patterns emerge.
OSINT tools for modern investigators and security professionals.
© 2026 Phantom Helix Intelligence. All rights reserved.
Made with ❤️ for the OSINT community