Phantom Helix Logo

Phantom Helix Intelligence

SIERRA Invoker ScriptDevelopment Guide

Learn how to create custom Invoker scripts to extend SIERRA's functionality and automate your investigation workflows.

Start with the beginner tutorialContinue to reference

What are Invoker Scripts?

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 Prompt for LLMs

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.

Script Structure

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:

Parameter types at a glance

STRING

Text from the user or graph

Use for domains, usernames, IDs, API keys, prompts, and short investigation values.

FILE

A selected local file path

Use when your script expects a normal path to a user-selected file on disk.

IMAGE

A 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.

COLLECTION

A 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]

yaml

1
2
3
4
5
6
7
8
9
10
SCRIPTS:
  - 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
21
import 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 payload shape

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.

  • Nodes usually include id, type, position, optional parentId, and data. Text content is usually at data.content.
  • Nested collections appear as nodes with type: collectionEntity. Their child nodes point back with parentId.
  • Edges include 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
10
SCRIPTS:
  - 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
52
import 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."]
        }
    ],
}))

Protocol Versions

SIERRA currently supports two invoker output contracts:V1 for batch/final JSON, and V2 for incremental node emission.

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.

V1 Output Formats

For the default V1 contract, your script should return JSON in one of these supported formats:

🌳 Tree Type Output

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"
            ]
        }
    ]
}

🕸️ Network Type Output

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" }
  ]
}

⚠️ Error Handling

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"
}

V2 Tutorial

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
14
PATHS:
  - /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
33
import 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.

Complete Example

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
14
PATHS:
  - /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}"

Best Practices

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

Ready to Build Your First Invoker?

Start building Invokers to supercharge your investigations, from quick lookups to complex multi-step automations.

Phantom Helix

In silence, patterns emerge.

OSINT tools for modern investigators and security professionals.

Product

SIERRAHelixDownloadSIERRA Cloud PlansCloud Invoker CatalogInvoker TutorialDocumentation

© 2026 Phantom Helix Intelligence. All rights reserved.

Made with ❤️ for the OSINT community