# Audio
Source: https://docs.mainly.ai/controls/audio
The Audio control is used to display an audio player.
This control is view-only. It is not interactive.
```
Audio(src=None)
```
# Example
```python theme={null}
from mirmod.controls import Audio
from mirmod import miranda
import json
@wob.receiver("state","audio",control=Audio(),hidden=True,connectable=False)
def get_audio(self, audio_data):
pass
@wob.execute()
def execute(self):
# Audio source can be a URL or base64 data URI
audio_url = "https://example.com/audio.mp3"
ecx = miranda.get_execution_context()
ob = ecx.get_current_wob()
sc = ecx.get_security_context()
miranda.update_api(
sc, ob,
"RECEIVER", "audio", "state",
value=audio_url, connectable=False, hidden=True
)
miranda.notify_gui(sc, json.dumps({
"action": "update[VIEW]",
"data": { "id": ob.id, "metadata_id" : ob.metadata_id }
}))
```
# Parameters
| Name | Type | Description |
| ---- | ------ | -------------------------------------------------- |
| src | string | Source URL or data URI of the audio. Default None. |
# ChatPreview
Source: https://docs.mainly.ai/controls/chatpreview
The ChatPreview control displays a preview of chat conversations.
```
ChatPreview()
```
# Example
```python theme={null}
from mirmod.controls import ChatPreview
@wob.receiver("state", "preview", control=ChatPreview(), hidden=True, connectable=False)
def chat_preview(self, data):
pass
```
# Parameters
This control has no configurable parameters.
# Checkbox
Source: https://docs.mainly.ai/controls/checkbox
A toggle control for boolean (true/false) values.
```
Checkbox(checked=False)
```
# Example
```python theme={null}
from mirmod.controls import Checkbox
@wob.receiver("value", "enabled", control=Checkbox(checked=False))
def receive_input(self, i):
self.enabled = i.lower() == "true"
```
The Checkbox returns a string (`"true"` or `"false"`), not a boolean. Convert the value as shown in the example above.
# Parameters
| Name | Type | Description |
| ------- | ---- | ---------------------------------------------------------- |
| checked | bool | Initial checked state when the node loads. Default: False. |
# CodeEditor
Source: https://docs.mainly.ai/controls/codeeditor
The CodeEditor control provides a code input field with syntax highlighting.
```
CodeEditor(placeholder="", max_len=16000, rows=4, language="python")
```
# Example
```python theme={null}
from mirmod.controls import CodeEditor
@wob.receiver("value", "code", control=CodeEditor(placeholder="Enter your code here...", rows=10, language="python"))
def receive_code(self, code):
self.code = code
```
# Parameters
| Name | Type | Description |
| ----------- | ------ | --------------------------------------------------------------- |
| placeholder | string | Placeholder text shown when the editor is empty. Default empty. |
| max\_len | int | Maximum character length allowed. Default 16000. |
| rows | int | Number of visible rows in the editor. Default 4. |
| language | string | Syntax highlighting language. Default "python". |
# ContinueButton
Source: https://docs.mainly.ai/controls/continuebutton
The ContinueButton control displays a button that allows users to continue workflow execution.
```
ContinueButton(label="Continue", disabled=True)
```
# Example
```python theme={null}
from mirmod.controls import ContinueButton
@wob.receiver("state", "continue", control=ContinueButton(label="Continue Processing", disabled=False), hidden=False, connectable=False)
def continue_button(self, data):
pass
```
This control presents a button to the user which lets them continue execution from the current state. This is useful for creating interactive workflows that pause for user input.
# Parameters
| Name | Type | Description |
| -------- | ------ | ----------------------------------------------------- |
| label | string | The text displayed on the button. Default "Continue". |
| disabled | bool | Whether the button is disabled. Default True. |
# DynamicNodeCompileButton
Source: https://docs.mainly.ai/controls/dynamicnodecompilebutton
The DynamicNodeCompileButton control allows users to recompile a dynamic node based on the current control values.
```
DynamicNodeCompileButton(label="Update Dynamic Node")
```
# Example
```python theme={null}
from mirmod.controls import DynamicNodeCompileButton
@wob.receiver("state", "compile", control=DynamicNodeCompileButton(label="Refresh Node"), hidden=False, connectable=False)
def compile_button(self, data):
pass
```
This control presents a button that lets power users re-compile the node from the current state of the node's API within the designer. This enables dynamic nodes that can change their attributes depending on the state of other controls on the node.
# Parameters
| Name | Type | Description |
| ----- | ------ | ---------------------------------------------------------------- |
| label | string | The text displayed on the button. Default "Update Dynamic Node". |
# File
Source: https://docs.mainly.ai/controls/file
The File control allows users to upload files, or capture media using camera or microphone.
```
File(accept=None, capture=None)
```
# Example
```python theme={null}
from mirmod.controls import File
# Accept image files for upload
@wob.receiver("value", "image_file", control=File(accept="image/*"))
def receive_image(self, file_data):
self.image = file_data
# Capture photo from camera
@wob.receiver("value", "camera", control=File(accept="image/*", capture="camera"))
def receive_camera(self, photo):
self.photo = photo
# Capture audio from microphone
@wob.receiver("value", "microphone", control=File(accept="audio/*", capture="microphone"))
def receive_audio(self, audio):
self.audio = audio
```
# Parameters
| Name | Type | Description |
| ------- | ------ | --------------------------------------------------------------------------------------------------- |
| accept | string | MIME type filter for accepted files (e.g., "image/*", "audio/*", ".pdf"). Default None (all files). |
| capture | string | Media capture source: "camera" or "microphone". Default None (file upload only). |
The `capture` parameter must be either "camera" or "microphone" if specified. Any other value will raise an exception.
# Image
Source: https://docs.mainly.ai/controls/image
A control for displaying images on a node.
This is a display-only control.
```
Image(width=-1, height=-1)
```
# Example
This example loads an image from disk and displays it on the node.
```python theme={null}
from mirmod.controls import Image
from mirmod import miranda
import json
import base64
@wob.init()
def init(self):
self.image_path = None
@wob.receiver("value", "Image Path")
def set_image(self, i):
self.image_path = i
@wob.receiver("state", "image", control=Image(), hidden=True, connectable=False)
def get_image(self, image_data):
pass
@wob.execute()
def execute(self):
# Load and encode the image
if self.image_path:
with open(self.image_path, "rb") as f:
image_bytes = f.read()
image_url = "data:image/png;base64," + base64.b64encode(image_bytes).decode("utf-8")
else:
return
# Get execution context
ecx = miranda.get_execution_context()
ob = ecx.get_current_wob()
sc = ecx.get_security_context()
# Update the control with the image data
miranda.update_api(
sc, ob,
"RECEIVER", "image", "state",
value=image_url, connectable=False, hidden=True
)
# Notify the GUI to refresh the display
miranda.notify_gui(sc, json.dumps({
"action": "update[VIEW]",
"data": { "id": ob.id, "metadata_id": ob.metadata_id }
}))
```
# Parameters
| Name | Type | Description |
| ------ | ---- | ------------------------------------------------------------------- |
| width | int | Display width in pixels. Use -1 for automatic sizing. Default: -1. |
| height | int | Display height in pixels. Use -1 for automatic sizing. Default: -1. |
# ImageMask
Source: https://docs.mainly.ai/controls/imagemask
An annotation control for drawing polygon regions on images.
```
ImageMask(width=-1, height=-1, polygons=[], url=None, style=None)
```
# Example
```python theme={null}
from mirmod.controls import ImageMask
import json
style = json.dumps({
"point_color": "#f01010",
"line_color": "#e21010",
"surface_color": "#ff000033"
})
@wob.receiver("value", "mask", control=ImageMask(style=style), hidden=True, connectable=False)
def receive_mask(self, value):
self.mask_data = value
```
# Parameters
| Name | Type | Description |
| -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| width | int | Display width in pixels. Use -1 for automatic sizing. Default: -1. |
| height | int | Display height in pixels. Use -1 for automatic sizing. Default: -1. |
| polygons | list | Initial polygon data. Each polygon is an object with `polygons` (list of `{x, y}` points in normalized 0-1 coordinates) and `labels` (list of strings). |
| url | string | URL of the image to annotate. |
| style | string | JSON string defining annotation colors: `point_color`, `line_color`, and `surface_color`. |
# KVSelect
Source: https://docs.mainly.ai/controls/kv-select
A dropdown control that loads options dynamically from a remote JSON source.
```
KVSelect(source, placeholder="Select a value", type="secret")
```
# Example
```python theme={null}
from mirmod.controls import KVSelect
@wob.receiver("value", "api_key", control=KVSelect(source="https://api.example.com/keys", placeholder="Select an API key", type="secret"))
def receive_key(self, key):
self.api_key = key
```
# How It Works
The `source` URL should return a JSON array of objects with `key` and `value` properties:
```json theme={null}
[
{ "key": "Option 1", "value": "opt1" },
{ "key": "Option 2", "value": "opt2" }
]
```
When the user types in the search box, a `?search=` parameter is appended to the source URL, enabling server-side filtering.
# Parameters
| Name | Type | Description |
| ----------- | ------ | -------------------------------------------------------------------------- |
| source | string | **Required.** URL to a JSON endpoint returning key-value options. |
| placeholder | string | Hint text displayed when no option is selected. Default: "Select a value". |
| type | string | Data type: `"secret"` or `"model"`. Default: "secret". |
# Markdown
Source: https://docs.mainly.ai/controls/markdown
The Markdown control works like a textbox control but has two modes: one for rendering Markdown and one for editing text. Shift-click to change mode.
```
Markdown(text="", options={}, blocktag=None)
```
# Example
```python theme={null}
from mirmod.controls import Markdown
@wob.receiver("value", "input", control=Markdown())
def receive_input(self, i):
self.values = i
```
# Parameters
| Name | Type | Description |
| -------- | ------ | ---------------------------------------------------- |
| text | string | Initial text content. Default empty. |
| options | dict | JSON-serializable options dictionary. Default empty. |
| blocktag | string | Optional block tag identifier. Default None. |
# Notice
Source: https://docs.mainly.ai/controls/notice
The Notice control displays a notification message on the node.
```
Notice(level='info', message='')
```
# Example
```python theme={null}
from mirmod.controls import Notice
@wob.receiver("state", "notification", control=Notice(level='warning', message='Please enter valid data'), hidden=True, connectable=False)
def show_notice(self, data):
pass
```
# Parameters
| Name | Type | Description |
| ------- | ------ | -------------------------------------------------------------------- |
| level | string | Notification level: 'info', 'warning', 'error', etc. Default 'info'. |
| message | string | The message text to display. Default empty. |
# Plotly
Source: https://docs.mainly.ai/controls/plotly
A control for displaying interactive Plotly charts and graphs.
This is a display-only control. Users cannot interact with the chart data.
This control is experimental and may have unexpected behavior.
```
Plotly()
```
# Example
This example creates a bar chart from a dataset and displays it using the Plotly control.
```python theme={null}
from mirmod.controls import Plotly
from mirmod import miranda
import json
import plotly.express as px
@wob.init()
def init(self):
self.df = None
@wob.receiver("data", "Dataset")
def set_dataset(self, i):
self.df = i
@wob.receiver("state", "plotly", control=Plotly(), hidden=True, connectable=False)
def get_plotly(self, plotly_data):
pass
@wob.execute()
def execute(self):
# Create a Plotly figure
fig = px.bar(self.df)
fig_json = fig.to_json()
# Get execution context
ecx = miranda.get_execution_context()
ob = ecx.get_current_wob()
sc = ecx.get_security_context()
# Update the control with the chart data
miranda.update_api(
sc, ob,
"RECEIVER", "plotly", "state",
value=fig_json,
connectable=False,
hidden=True
)
# Notify the GUI to refresh the display
miranda.notify_gui(sc, json.dumps({
"action": "update[VIEW]",
"data": { "id": ob.id, "metadata_id": ob.metadata_id }
}))
```
# Parameters
This control has no configurable parameters. Chart configuration is handled through the Plotly figure object itself.
# RunUntilButton
Source: https://docs.mainly.ai/controls/rununtilbutton
The RunUntilButton control displays a button that executes the graph until the current node is reached.
```
RunUntilButton(label="Run Until this Node")
```
# Example
```python theme={null}
from mirmod.controls import RunUntilButton
@wob.receiver("state", "run", control=RunUntilButton(label="Execute to Here"), hidden=False, connectable=False)
def run_until(self, data):
pass
```
This control allows users to run the graph until the node is reached. It's useful for creating nodes that update their attributes based on their inputs during partial execution.
# Parameters
| Name | Type | Description |
| ----- | ------ | ---------------------------------------------------------------- |
| label | string | The text displayed on the button. Default "Run Until this Node". |
# Select
Source: https://docs.mainly.ai/controls/select
A dropdown control for selecting from a list of options.
```
Select(choices=[], placeholder="")
```
# Example
```python theme={null}
from mirmod.controls import Select
@wob.receiver("value", "input", control=Select(placeholder="Choose an option", choices=["a", "b", "c"]))
def receive_input(self, i):
self.selected = i
```
The Select control always returns a string. If you need a different type, convert the value in your receiver function or during execution.
# Parameters
| Name | Type | Description |
| ----------- | ------ | -------------------------------------------------- |
| choices | list | List of string options to display in the dropdown. |
| placeholder | string | Hint text displayed when no option is selected. |
# Slider
Source: https://docs.mainly.ai/controls/slider
The Slider control is used to select a numeric value from a range.
```
Slider(min=0.0, max=1.0, step=0.1)
```
# Example
```python theme={null}
from mirmod.controls import Slider
@wob.receiver("value", "input", control=Slider(min=0.0, max=100.0, step=1.0))
def receive_input(self, i):
self.value = float(i)
```
# Parameters
| Name | Type | Description |
| ---- | ----- | ------------------------------------------ |
| min | float | Minimum value of the slider. Default 0.0. |
| max | float | Maximum value of the slider. Default 1.0. |
| step | float | Step increment of the slider. Default 0.1. |
# Table
Source: https://docs.mainly.ai/controls/table
A control for displaying tabular data.
```
Table(columns=[], format='csv')
```
# Example
```python theme={null}
from mirmod.controls import Table
@wob.receiver("value", "data", control=Table(columns=["name", "age", "city"], format='csv'))
def receive_data(self, csv_data):
self.data = csv_data
```
Data should be provided as a CSV string or JSON array, matching the `format` parameter.
# Parameters
| Name | Type | Description |
| ------- | ------ | --------------------------------------------------- |
| columns | list | Column headers for the table. |
| format | string | Data format: `"csv"` or `"json"`. Default: `"csv"`. |
# Textbox
Source: https://docs.mainly.ai/controls/textbox
A text input control for entering single-line or multi-line text.
```
Textbox(placeholder="", min_len=0, max_len=16000, rows=1, regex=".*")
```
# Example
```python theme={null}
from mirmod.controls import Textbox
@wob.receiver("value", "input", control=Textbox(placeholder="a,b,c,..."))
def receive_input(self, i):
self.values = i.split(',')
```
The Textbox always returns a string. If you need numbers or JSON, parse the value in your receiver function or during execution.
# Parameters
| Name | Type | Description |
| ----------- | ------ | ----------------------------------------------------------------------------------------------------------------------- |
| placeholder | string | Hint text displayed when the field is empty. |
| min\_len | int | Minimum required character count. Shows an error if not met. Default: 0. |
| max\_len | int | Maximum allowed character count. Shows an error if exceeded. Default: 16000. |
| rows | int | Number of visible text rows. Values greater than 1 render a resizable textarea. Default: 1. |
| regex | string | Regular expression pattern for input validation. Shows an error if the input doesn't match. Default: ".\*" (any input). |
# Video
Source: https://docs.mainly.ai/controls/video
The Video control is used to display a video player.
This control is view-only. It is not interactive.
```
Video(width=-1, height=-1, type="video/mp4", src=None)
```
# Example
```python theme={null}
from mirmod.controls import Video
from mirmod import miranda
import json
@wob.receiver("state","video",control=Video(width=640, height=480),hidden=True,connectable=False)
def get_video(self, video_data):
pass
@wob.execute()
def execute(self):
# Video source can be a URL or base64 data URI
video_url = "https://example.com/video.mp4"
ecx = miranda.get_execution_context()
ob = ecx.get_current_wob()
sc = ecx.get_security_context()
miranda.update_api(
sc, ob,
"RECEIVER", "video", "state",
value=video_url, connectable=False, hidden=True
)
miranda.notify_gui(sc, json.dumps({
"action": "update[VIEW]",
"data": { "id": ob.id, "metadata_id" : ob.metadata_id }
}))
```
# Parameters
| Name | Type | Description |
| ------ | ------ | -------------------------------------------------- |
| width | int | Display width in pixels. -1 for auto. Default -1. |
| height | int | Display height in pixels. -1 for auto. Default -1. |
| type | string | MIME type of the video. Default "video/mp4". |
| src | string | Source URL or data URI of the video. Default None. |
# Attributes
Source: https://docs.mainly.ai/core_concepts/attributes
Attributes
All `Code_block` objects have a field, `api`, that is a JSON representaition of the code block's attributes. These attributes define the behaviour of the code block when it is rendered as a node in the workspace.
Example of attributes fields defining two receivers with textbox controls and one transmitter:
```python theme={null}
{
[..] # other fields not relevant to the attributes
"attributes": [
{"kind": "string",
"name": "template prompt",
"value": "",
"hidden": false,
"control":
{"kind": "textbox",
"rows": 20,
"regex": ".*",
"max_len": 3024,
"min_len": 0,
"placeholder": ""},
"direction": "RECEIVER"},
{"kind": "string",
"name": "user prompt",
"value": null,
"hidden": false,
"control":
{"kind": "textbox",
"rows": 20,
"regex": ".*",
"max_len": 3024,
"min_len": 0,
"placeholder": ""},
"direction": "RECEIVER"},
{"kind": "string",
"name": "processed prompt",
"value": null,
"hidden": false,
"control": null,
"direction": "TRANSMITTER"}
]
}
```
# Controls
Source: https://docs.mainly.ai/core_concepts/controls
Configurable input and display elements for nodes
A [receiver](/core_concepts/attributes#recievers) can be configured with a control which adds special GUI features to the node. There are several controls to choose from:
## Input Controls
A text field or textbox for entering text.
A dropdown list for selecting a menu item.
A Select control which takes its options from a key-value dictionary.
A slider for selecting a value from a range.
A checkbox for selecting a boolean value.
A file upload control with camera and microphone capture support.
A code editor with syntax highlighting.
An annotation tool for image masking.
## Display Controls
An image control for displaying an image.
A video player control.
An audio player control.
A Markdown control for displaying formatted text.
A Plotly control for displaying a Plotly graph.
A Table control for displaying tabular data.
A notification message display.
A preview display for chat conversations.
## Action Controls (Advanced)
A button to continue workflow execution.
A button to run the graph until this node.
A button to recompile dynamic nodes.
# Fields
Source: https://docs.mainly.ai/core_concepts/fields
Fields are how loops are created in a workflow graph.
When you want to build loops in MainlyAI you use iterator fields. Fields are defined by a field [Transmitter](core_concepts/attributes#recievers) and a field [Receiver](core_concepts/attributes#recievers). Any nodes between these will be part of the Field and will be iterated until either the Transmitter runs out of data or the Receiver decides its conditions are met.
To mark a Transmitter or Reciever as a feild, set `is_field=True` on the attribute decorator.
```python theme={null}
@wob.transmitter("value", "name", is_field=True)
def transmit_values(self):
...
```
```python theme={null}
@wob.receiver("value", "name", is_field=True)
def receiver_values(self):
...
```
A field Transmitter is expected to return a tuple. The first element is a error handler and it can be None. The second element is a regular python iterator.
Here's an example of a node that splits a string by commas and returns the strings as a Field.
```python theme={null}
@wob.init()
def init(self):
self.values = ['a', 'b', 'c']
self.iterator = None
@wob.receiver("value","input")
def receive_input(self, i):
self.values = i.split(',')
@wob.transmitter("value", "output", is_field=True)
def transmit_values(self):
return self.iterator
@wob.execute()
def execute(self):
self.iterator = (None, iter(self.values))
```
This pattern will run the nodes A and B for every a,b,c.
Writing a field Receiver is similar to writing a normal Receiver, it will just be called multiple times until the Transmitter runs out of data. Here's an example of a node that prints each value in a Field.
```python theme={null}
@wob.receiver("value", "input", is_field=True)
def receive_values(self, i):
print(i)
```
After all values have been received, execution will continue like normal. However, the Reciever can also use one of the Execution Context hooks to break out of the loop early. Here's an example of a node that will only recieve the first two values in a Field, then break back into normal execution.
```python theme={null}
@wob.receiver("value", "input", is_field=True)
def receive_values(self, i):
print(i)
self.count_received += 1
self.aggr.append(i) # if you want to aggregate the things you iterate over.
```
## Error handlers
A field can use an error handler for controlling how errors are handled in the field.
Example:
```python theme={null}
from mirmod import miranda
@wob.init()
def init(self):
self.value = None
self.itr = None
@wob.receiver("value","input")
def receive_value(self, value):
self.value = value
@wob.transmitter("value", "output", is_field=True)
def transmit_value(self):
return self.itr
@wob.execute()
async def execute(self):
class MyExceptionHandler:
async def __call__(self, execution_context, executeNodeFunc, wob):
try:
await executeNodeFunc(wob)
except Exception as e:
print ("Cool!")
print (e)
return miranda.F_NEXT_ELEMENT
finally:
pass
return miranda.F_PROCEED
self.itr = (MyExceptionHandler(), iter([1,2,3,4,5]))
```
MyExceptionHandler will trap any error, write "Cool!" to the log and then continue to the next element in the iterator.
The following behvaiors are currently supported:
| Return status code | Description |
| ------------------ | --------------------------------------------------------------- |
| F\_NEXT\_ELEMENT | Continue with the next element on the iterator |
| F\_PROCEED | Continue with the next node in the field (normal behavior) |
| F\_EXIT | Exhaust the iterator and move to the receiver node execution |
| F\_TRY\_AGAIN | Repeat execution of the node that raised the exception |
| F\_RESTART | Reset and restart the iterator from the beginning and try again |
## Advanced setup
Some rules apply for all iterator fields:
* Inbound edges connected to nodes on a different branch are only executed once before the field transmitter. These nodes are called initialization nodes.
* Outbound edges from inside the field to a leaf node are repeatedly executed on each iteration.
* Don't connect outbound edges to nodes outside of the field. Instead collect the result of the field operation in the field receiver and connect your out-edges on this node.
* Fields can be nested.
* If there are multiple field transmitters on a node, the node is a dispatch node and it follows different rules.
* For regular fields only use one field transmitter and one field receiver.
# Knowledge Objects
Source: https://docs.mainly.ai/core_concepts/knowledge_objects
Knowledge Objects
The Knowledge object is a WOB that representes an entire workflow graph. Users can access the object via the sql view `v_knowledge_objects` and the python object `miranda.Knowledge_object`.
# Processors
Source: https://docs.mainly.ai/core_concepts/processors
Processors
When you press the play button to run a workflow a a pod is allocated from the CPU-cluster. The pod is a docker container which runs a program called the processor.
The processor is responsible for downloading the node graph from the MainlyAI database and execute it. The pod has its own local disk which the node graph can write and read from,
and the process will write the code of all the nodes as files to the disk before exxecuting them. These files are name "WOB-xxxx.py" where xxxx is the metadata id of the node.
Before execution the processor constructs an execution plan which determines the order in which the nodes are executed. The execution plan is printed in the log as a debug message.
If the graph doesn't execute as expected then the first thing you should check is the execution plan.
The processor also provides a service called the execution context which allows a node to inspect the graph it is executing in.
# Workflow Objects (WOBs)
Source: https://docs.mainly.ai/core_concepts/workflow_objects
Workflow Objects
Workflow Objects (WOBs) are the core building blocks of the MainlyAI data model. They provide an ORM-style abstraction over the underlying database tables, which primarily consist of shared metadata tables and a set of workflow-specific tables such as `code`, `knowledge_object`, `compute_resource_groups`, and `project`.
Each WOB is exposed to the user as a database view that enforces access control based on information stored in ACL tables. Modifications to a WOB are performed via stored procedures that execute in a privileged context, governed by the same ACL rules. This database interface is then wrapped in a corresponding Python object—such as `Code_block`, `Knowledge_object`, `Model`, or `Project`—which serves as the primary API for interacting with the object.
## Edges
All WOBs are connected in graphs via an edge table (edges). Users interact with these connections through the `v_edges` view, and WOBs can be linked or unlinked using the MainlyAI functions `miranda.link()` and `miranda.unlink()`.
| Color | Data types | Flow direction | Use case |
| :----- | :----------------- | :------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Red | String, number | Forward | Used for prompts, settings, text, code |
| Green | JSON or dicts | Forward | Program state, objects that change forward |
| Blue | Dataset, DataFrame | Forward | Relational data that change forward |
| Purple | API, ML models | Backwards | The leaf nodes of a purple branch is a subprogram that has its own state and it can receive and propagate information. Note that all objects are passed by reference which is important in concurrent execution. |
# Glossary
Source: https://docs.mainly.ai/glossary
Terms and concepts used in The MainlyAI Platform
## Workflows
[Workflows](/core_concepts/knowledge_objects), also known as Knowledge Objects, define workflow graphs and their associated metadata and options.
## Workflow Objects
[Workflow Objects](/core_concepts/workflow_objects) are individual nodes that hold code along with a list of the inputs and outputs, called [Attributes](/core_concepts/attributes).
## Attributes
[Attributes](/core_concepts/attributes) define the inputs ([Recievers](/core_concepts/attributes#recievers)) and outputs ([Transmitters](/core_concepts/attributes#transmitters)) of [Workflow Objects](/core_concepts/workflow_objects). They are defined by a name, a type, and a direction. Attributes can also expose [Controls](/core_concepts/attributes#controls), which specifies how the attribute is displayed in Designer.
## Controls
[Controls](/core_concepts/controls) are used to specify how an [Attribute](/core_concepts/attributes) is displayed in Designer. Controls often allows users to modify the value of an attribute, but in some cases they only display the contents of the attribute. Some examples of controls include [Textbox](/controls/textbox), [Select](/controls/select), [Slider](/controls/slider), and [Plotly](/controls/plotly).
## Processors
[Processors](/processors) are ephemeral containers that run [Workflow Objects](/core_concepts/workflow_objects). Processors are tied to [Workflows](/core_concepts/knowledge_objects), who define the workflow graph that the processor will run.
# Using github for saving nodes
Source: https://docs.mainly.ai/guides/using_git
This guide teaches you how save a node to a github repository.
First start by creating a new repository on github. Then copy the SSH URL of the repository to the clipboard.
Let start with a simple node that prints a message to the console.
Click on down arrow in the top right corner to flip the node to the backside.
As you enter the URL for the git repository, another field will appear where you can enter the branch name.
Next press we need to add a ssh key to the MainlyAI secret store. Access the secret store by clicking on the MainlyAI logo in the top right corner of the workspace. You will see a meny pop out. Click on the "Account" option.
In the left tab click on the "Secrets" option.
Click on the + button to add a new secret and call it "\$git\_ssh\_key". The value should be a JSON object like this:
\{ "ssh\_key" : "\" }
If your ssh key is password protected you will be prompted to enter a password when pulling and pusing to the repository. If you don't want to enter a password everytime you can add another secret to the secret store called '\$ssh\_key\_password'.
Now we can go back to the workbench and click the "Push" button on the back of the node. This will pop up a dialogue window where you can view what will happen after the push.
If you don't like what you see you can just close down the window and nothing will change.
If you want to continue you click the "Push" button again.
After a few seconds the window will close and you can continue to work. Pulling from the repository is done in the same way by pressing the "Pull" button.
# Using LLM tools
Source: https://docs.mainly.ai/guides/using_llm_tools
This guide teaches you how to use LLM tools in Mainly AI.
Tools are functions invoked by the LLM during inference. Note that not all LLMs support tools, and Mainly AI does not support tools for every capable model.
Currently, the `llm.generate_text` node supports tools for:
* OpenAI's GPT models
* Google's Gemini models
* BergetAI's GPT OSS model
To create a new tool, drag a wire from the `tools` socket onto the canvas and select `llm.construct_tool`.
This creates a template for your implementation. The implementation node is an API node that acts as the interface for your tool.
The only method you need to implement is `async __call__`.
Example:
```python theme={null}
from mirmod import miranda
@wob.init()
def init(self):
self.api = None
@wob.transmitter("model", "output")
def transmit_value(self):
return self.api
@wob.execute()
async def execute(self):
class API:
async def __call__(self, city:str):
return "It is going to be bad weather in {}".format(city)
self.api = API()
```
The signature of the `__call__` method defines the parameters the LLM should send to the tool.
In some cases, you may need access to the internal state of the `llm.generate_text` node. To achieve this, define a function with the signature `def inner_call(http=None, messages=None)` and return this function instead of a standard string response.
Returning this function prompts the `llm.generate_text` node to invoke it with the `http` and `messages` arguments. This provides access to HTTP-specific context and the message history, which you can then search or forward to another LLM.
Example:
```python theme={null}
from mirmod import miranda
@wob.init()
def init(self):
self.api = None
@wob.transmitter("model", "output")
def transmit_value(self):
return self.api
@wob.execute()
async def execute(self):
class API:
async def __call__(self, city:str):
async def inner_call(http=None, messages=None):
nonlocal city
await http.event("delta","Send text through the http request API")
return "It is going to be bad weather in {}".format(city)
return inner_call
self.api = API()
```
Tools can be useful even if they perform no functional action. For instance, an empty tool named "take\_notes" will still be called by the LLM if it deems it relevant. This tool call is recorded in the message history, which the LLM sees. Consequently, the LLM "believes" it has taken notes, which influences its future inference. In this context, empty tools function as a "thinking" mechanism.
## llm.construct\_tool
The implementation of the tool is wrapped behind a `llm.construct_tool` node. This node is an API node that acts as the interface for your tool. The only method you need to implement is `async __call__`. The `llm.construct_tool` node will automatically generate the necessary metadata for the tool, including the tool's name, description, and parameters. The description can be provided by adding a `def description(self) return ""` method to your API class, or it can be typed directly in the `description` field of the `llm.construct_tool` node.
The name of the tool, from the point of view of the LLM, is written in the `name` field of the `llm.construct_tool` node.
You can write the description of the tool in the tool implementatain node as well, and if you do then on the next exeuctioon thethe `description` field of the `llm.construct_tool` node will be updated with the description from the tool implementation node.
Example:
```python theme={null}
@wob.execute()
async def execute(self):
class API:
def description(self):
return "Returns a really bad weather report from the specified city."
async def __call__(self, city:str):
async def inner_call(http=None, messages=None):
nonlocal city
await http.event("delta","Send text through the http request API")
return "It is going to be bad weather in {}".format(city)
return inner_call
self.api = API()
```
# Using wob notifications
Source: https://docs.mainly.ai/guides/using_notifications
This guide teaches you how to use wob notifications.
A node can emit notifications using the wob-api.
Example:
```python theme={null}
@wob.execute()
def execute(self):
wob.status.waiting("Summarising...", timeout=3000, color="#22dd22", icon="Check")
< do stuff >
wob.status.clear()
```
The following methods are available for the status object:
* waiting(self, name, icon="", color="", timeout=None, is\_global=False)
* info(self, name, icon="", color="", timeout=None, is\_global=False)
* progress(self, name, value, max\_value=0, icon="", color="", timeout=None, is\_global=False)
* clear(self, is\_global=False)
# Using built in RabbitMQ queues
Source: https://docs.mainly.ai/guides/using_rabbitmq
This guide teaches you how to use built in RabbitMQ queues to send messages between workflows or controls.
All workflows have access to the `nodes` topic exchanges. Each node can create one queue and access to this queue is controlled by who have write access to this node. Queues have a random number appended to them so that they will be unique per replica instance. They can all listen on the same topic and act as workers.
Set up a worker like this:
```python theme={null}
from mirmod import miranda
from mirmod.mq import register_queue
@wob.init()
def init(self):
self.value = None
@wob.transmitter("value", "output")
def transmit_value(self):
return self.value
@wob.execute()
async def execute(self):
ecx = miranda.get_execution_context()
qh = await register_queue(ecx)
print("Producers should use topic=", qh.topic)
self.value = await qh.consume()
```
Running this node will give you a topic (the same as the metadata\_id of the consumer node).
`register_queue(execution_context, ...)` can take the following named parameters:
| Name | Type | Description |
| :----------- | :------ | :------------------------------------------------------------------------------------------------------------------------------------------ |
| name | string | The name of the queue. Usually not something you change because it is tied to how the application autenticates using node level privielges. |
| topic | string | The topic to bind to. |
| durable | boolean | Will the queue survive a broker restart. |
| exclusive | boolean | Used by only one connection and the queue will be deleted when that connection closes |
| auto\_delete | boolean | Queue that has had at least one consumer is deleted when last consumer unsubscribes |
| message\_ttl | int | How long a messages can live if no consumer requests it. |
| max\_length | int | Max length of a messages |
Use this topic with the producer as follows:
```python theme={null}
from mirmod import miranda
from mirmod.mq import publish
@wob.init()
def init(self):
self.value = None
@wob.transmitter("value", "output")
def transmit_value(self):
return self.value
@wob.execute()
async def execute(self):
topic = "42578" # The topic we got from the consumer node
ecx = miranda.get_execution_context()
await publish(ecx, message={"Hello": "world"}, topic=topic)
```
Start the consumer in a loop like this:
The iterator can look something like this:
```python theme={null}
@wob.transmitter("value", "output", is_field=True)
def transmit_value(self):
return self.itr
@wob.execute()
async def execute(self):
class DummyItr:
def __init__(self):
pass
def __iter__(self):
return self
def __next__(self):
return ":)" # loop forever
self.itr = DummyItr()
```
# Visualizing a Dataset
Source: https://docs.mainly.ai/guides/visualizing-a-dataset
This guide teaches you how to load a CSV file from a URL and how to visualize it using Plotly.
[View Project in Designer](https://platform.mainly.ai/designer/projects/16/graphs/43)
This guide will use three [Prefabs](/workflow_objects#prefabs) and one custom [Workflow Object](/workflow_objects#custom) to load a CSV file from GitHub over HTTP and visualize it as a bar chart using Plotly.
Begin by importing the `net.fetch` Prefab by dragging it from the Prefab Library on the left-hand side of the workspace into the workflow graph.
In this example, we will be using a sample dataset of population statistics with the following columns:
* `id` - A unique identifier for each row
* `gender` - Possible values are `Male`, `Female`, `Bigender`, `Agender`, and `Genderfluid`.
* `age` - A positive integer
* `country` - A two letter country code
The dataset is available as a CSV file on GitHub at the following URL: [`https://raw.githubusercontent.com/mainly-ai/the-lab/main/datasets/population_stats.csv`](https://raw.githubusercontent.com/mainly-ai/the-lab/main/datasets/population_stats.csv)
Enter this dataset URL into the `URL` field of the `net.fetch` Prefab. Then import the `util.show_text` prefab and connect the `Body` transmitter on the `net.fetch` Prefab to the `input_2` (which takes a String) receiver on the `miranda_test.printer` Prefab.
Now let's run the project and look at the output in the logs using the [Processor](/processor) panel on the right of the workspace.
You should now see the text contents of the CSV file in the logs. To visualize this data, we will first need to parse it into a format that Plotly can understand, such as a Pandas DataFrame. To do this, we will use the `pandas.from_csv` Prefab. Import it from the Prefab Library and connect the `Body` transmitter on the `net.fetch` Prefab to the `CSV` receiver on the `pandas.from_csv` Prefab. Then you can connect the `Dataframe` transmitter on the `pandas.from_csv` Prefab to the `input_1` (which takes a Dataframe) receiver on the `miranda_test.printer` Prefab.
However, if we try to plot this data directly using Plotly, we will get an error or incoherent results. This is because the data is high-dimensional. Let's write a custom [Workflow Object (Node)](/workflow_objects#custom) to aggregate the data and visualize it as a bar chart. In this example, we will group the data by `country` and average the `age` column.
Create a new Node by right-clicking on the workspace and selecting `Create Node`. Then right-click the node and select `Edit Code` to begin implementing our own logic. By default, the new Node contains some boilerplate code to get you started.
```python theme={null}
from mirmod import miranda
@wob.init()
def init(self):
self.value = None
@wob.receiver("value","input")
def receive_value(self, value):
self.value = value
@wob.transmitter("value", "output")
def transmit_value(self):
return self.value
@wob.execute()
def execute(self):
print(f"self.value = '{self.value}'")
```
These are the four main parts of a Workflow Object, which are evaluated in the following order:
1. `init` - This is the constructor for the Workflow Object. It is called when the object is created and can be used to initialize any variables.
2. `receiver` - Receives data from other Workflow Objects or from Controls.
3. `execute` - This is the main function of the Workflow Object. It is called when all the receivers have been called.
4. `transmitter` - Sends data to other Workflow Objects.
Let's initialize our Workflow Object. We're gonna want two variables, `self.df` to store the DataFrame received from the `pandas.from_csv` Prefab and `self.transformed` to store the transformed DataFrame.
```python theme={null}
@wob.init()
def init(self):
self.df = None
self.transformed = None
```
The default code is configured to receive and transmit strings. We will need to modify this to use DataFrames. Let's also change the names to better reflect the purpose of the Node and make sure we're setting and returning the right variables.
```python theme={null}
@wob.receiver("data", "Dataframe")
def receive_value(self, value):
self.df = value
@wob.transmitter("data", "Population by Country")
def transmit_value(self):
return self.transformed
```
Now let's write the logic to transform the DataFrame. We will use the `groupby` method to group the data by `country` and then use the `mean` method to average the `age` column.
```python theme={null}
@wob.execute()
def execute(self):
self.transformed = self.df.groupby(['country'])['age'].mean().sort_values()
```
Now lets plot this data using Plotly. Import the `plotly.bar` Prefab and connect the `Population by Country` transmitter on the custom Workflow Object to the `Dataset` receiver on the `plotly.bar` Prefab. Then run the project, and you should see a bar chart of the average age by country appear on the `plotly.bar` node.
# Writing, debugging and deploying webservices
Source: https://docs.mainly.ai/guides/webservices
This guide teaches you how to use the server_http node to build a web service.
[View Project in Designer](https://platform.mainly.ai/designer/projects/31/graphs/236)
In this exercise, we will create a simple web service which streams data to the client. The projects main node is the `web.async_http` node which listens for incoming HTTP requests. It accepts an authentication node and needs to be terminated by a `web.async_finish_request` node.
Begin by dragging the following nodes onto the workspace:
* `web.async_http`
* `web.async_finish_request`
Then create a new node by right clicking on the workspace and selecting `Create Node`. This node will be used to generate the data that will be streamed to the client. Rename the new code and call it `Hello, world`.
Connect them according to the image below:
Right click on the `Hello, world`-node and commit the following code:
```python theme={null}
from mirmod import miranda
g_counter = 0
@wob.init()
def init(self):
self.http = None
wob.passthrough("state","http","http")
@wob.execute()
async def execute(self):
global g_counter
param = self.http.payload["prompt"] if "prompt" in self.http.payload else "no prompt given"
assert self.http != None
print ("g_counter= ", g_counter)
await self.http.event("data","{} {}".format( param, g_counter))
g_counter += 1
```
Click the run button on the right sidebar and the HTTP server will start listening on port 443 on the following address: `https://.p.mainly.cloud`
The `` you find if you click the job indocator in the top right of the workspace screen:
Here we see that the ID is 01hwawbp7aam0cx8jcwzt38vtj and if we go to [https://01hwawbp7aam0cx8jcwzt38vtj.p.mainly.cloud/docs](https://01hwawbp7aam0cx8jcwzt38vtj.p.mainly.cloud/docs) we can verify that the service is running. Next we need to write the client code. For this we need Visual Studio code or similar and an installed python environment on our desktop.
This is the client code we're going to use:
```python theme={null}
import requests
def prompt_mainly(endpoint, prompt):
url = f"https://01hwawbp7aam0cx8jcwzt38vtj.p.mainly.cloud/{endpoint}"
payload = { "prompt": prompt }
resp = requests.post(url, json=payload, stream=True)
d= ""
for r in resp:
d += r.decode("utf8")
print (d)
```
We call the function like this:
```python theme={null}
prompt_mainly("stream_run_payload","Hello, world")
```
We should expect to see the following output:
```
event: data
data: "Hello, world 0"
```
# Resize nodes
Source: https://docs.mainly.ai/how-to-use/resize-nodes
How to resize nodes that are resizable
Some nodes are resizable. Nodes with textboxes that have bolded corner to the bottom right are reziseable and you click and drag that corner to rezise.
# Introduction
Source: https://docs.mainly.ai/introduction
MainlyAI is a node-based platform for building AI workflows.
# Features and manifesto
Our manifesto posits that AI is the next layer in your infrastructure stack. For this layer to be sustainable, it requires a structure that is transparent, controlled, and deeply rooted in rigorous engineering. Below are our features, categorized by the four pillars of **Grounded Vibe Coding**.
***
## 1. The Graph as a First-Class Object
*Mastering complexity through visualization.*
In a world where AI generates the code, human understanding of the system becomes the bottleneck. We elevate the graph as the primary interface.
Solid architectural building blocks and solution templates designed to scale.
Navigate complexity by quickly finding any code in any node in your workflow.
Develop, tweak, and fine-tune logic directly within the nodes in a one-stop shop.
Full transparency and searchability across code, assets, and team members.
***
## 2. Human Strategy & Control
*Steer the AI; don't let it take the helm.*
The promise of automation is to eliminate repetition, but the strategy must always remain human.
Use our code agent or write your own custom version that acts on your specific terms and instructions.
Deep-dive debugging to understand exactly what is happening under the hood.
Run your workflows locally for full control and rapid iteration before deployment. With our k3s plugin you can manage a whole cluster.
Orchestrate and run customized, local Large Language Models (LLMs).
***
## 3. The Infrastructure Foundation
*Reliability and security rooted deep within the data model.*
Security isn't "patch-on" middleware—it’s a core part of the architecture. We build on proven tech to eliminate technical debt.
Database security model for granular and secure data sharing across organizations.
S3-compatible storage and SSH access to manage and protect your data capital.
Seamless management of external API keys and resources with deep integration.
Industry-standard storage, now enhanced with Vector Store support.
Enterprise-grade network isolation and secure connectivity for your entire stack.
***
## 4. Hybrid Execution & Scalability
*Seamless orchestration across environments.*
To avoid friction, the system must be able to breathe and grow without being torn up by the roots.
A central gateway to any LLM via a universal credit system.
On-demand access to GPU, CPU, and disk resources for heavy workloads.
Robust messaging to scale transactions and asynchronous logic flows let your workflows speak between each other or with GUI controls.
Move workflows seamlessly from local environments to on-premise clusters.
Simple B2B integration for resource management, auditing, and budgeting.
***
> **The Grounded Vibe:** "Execution is automated, but strategy stays in your hands. We make systems transparent, predictable, and genuinely human to understand."
## Get to know The Platform
MainlyAI is a node-based platform for building AI workflows. What does this mean?
Get to know the terminology used in the MainlyAI Platform.
Dynamic Input & Display Elements on Nodes.
## Example Projects
Learn how to visualize a CSV dataset loaded from a URL.
Learn how to visualize a CSV dataset loaded from a URL.
# Mainly LLM
Source: https://docs.mainly.ai/mainlyllm
Out of the box LLM models
The `llm.generate_text` node will default to our LLM proxy, which bills usage directly from the Credits included in your subscription to the MainlyAI Platform. This also means that you don't have to manually set up API keys or other credentials. To use your own models, simply connect them to the `llm` reciever.
## Using in your own nodes
The Mainly LLM proxy can also be used directly in your own nodes without any additional setup.
### Simple Example
```py theme={null}
from mirmod import llm
...
@wob.execute()
async def execute(self):
messages = [
{"role": "system", "content": "You are a helpful assistant."},
{"role": "user", "content": "What's 9+10?"},
{"role": "assistant", "content": "21"},
{"role": "user", "content": "Are you sure???"},
]
model = llm("openai/gpt-5.2")
.raw_response() # whether to return raw dicts or classes
response = await model.send(messages) # returns a list of new messages
```
### Configuring the model
```py theme={null}
from mirmod.miranda_llm import LLMReasoningEffort
model = llm("openai/gpt-5.2")
.raw_response() # whether to return raw dicts or classes
.model_parameters({
"max_tokens": 100,
"temperature": 0.7,
"top_p": 0.9,
"top_k": 40,
"parallel_tool_calls": True,
"reasoning_effort": LLMReasoningEffort.MIMIMAL
}) # all of these are optional
```
Looking for more models? Let us know by emailing us at [contact@mainly.ai](mailto:contact@mainly.ai)!
# Thinking with Nodes
Source: https://docs.mainly.ai/thinking_with_nodes
MainlyAI is a node-based platform for building AI workflows. What does this mean?
## This is a node
*Also known as a Workflow Object, or WOB for short.*
## Nodes are connected by Edges
A transmitter can be connected to multiple receivers, but a receiver can only be connected to one transmitter.
## Edges move data between nodes
Edges move data from transmitters to receivers. Nodes are ran sequentially depth-first.
## Edges care about types
Transmitters and receivers can have different types. Edges can only connect transmitters and receivers of the same type. Each type has a different color.
* **value** - individual values, such as strings, numbers, and vectors.
* **data** - collections of values, such as tables, datasets, and tensors.
* **state** - JSON objects and classes, such as LLM messages and HTTP requests.
* **model** - functions/functors that can be called by nodes later
## Receivers can have controls
Controls are configurable input and display elements for nodes. They can be added onto receivers using the `control` argument. If an edge is connected to a receiver with a control, the control will disappear and the value will be taken from the edge instead.
[Read more about controls](/core_concepts/controls).
```python theme={null}
from mirmod.controls import Textbox
@wob.receiver("value", "input", control=Textbox())
def receive_value(self, value):
self.value = value
```
## Fields let nodes run in loops
They are defined by a pair of receivers and transmitters that are both marked as `is_field=True`. Visually, you can tell that a transmitter or receiver is a field transmitter/receiver by their pointier shape. Nodes within a field are continously run until the field transmitter runs out of values to transmit.
[Read more about fields](/core_concepts/fields).