Plugins

July 18, 2026 ยท View on GitHub

Overview

This document describes the xyOps Plugin System. You can extend xyOps with the inclusion of Plugins, either by writing them yourself (in any language!), or by finding one on the Plugin Marketplace.

Plugin Types

Here are the Plugin Types available:

Event Plugins

Event Plugins are the main type of Plugin in xyOps, as they actually are the code that "runs" jobs. When events launch a job, either standalone or as part of a workflow, they all point to a specific Event Plugin, which executes the code that constitutes the job itself. Event Plugins run on the target server running the job, and are launched as child processes from xySat, our remote agent.

Several built-in Event Plugins ship with xyOps. They are:

Plugin NameDescription
Shell PluginThe Shell Plugin allows you to easily create events that execute arbitrary shell code, without having to learn the xyOps Plugin API.
HTTP Request PluginThe HTTP Plugin can send HTTP requests to any URL, and supports a variety of protocols and options, including custom headers and custom body content.
Test PluginThe Test Plugin exists mainly to test xyOps, but it can also be useful for testing pieces of workflows. It outputs sample data and optionally a sample file, which are passed to downstream events, if connected.
Fire Web Hook PluginThe Fire Web Hook Plugin fires one of your configured xyOps web hooks as a standard job, so workflows can branch or fail based on the web hook result.
Docker PluginThe Docker Plugin allows you to run custom scripts inside a Docker container. Similar to the Shell Plugin, you can specify any custom code to run, and in any language, as long as it supports a Shebang line.

To write your own Event Plugin, all you need is to provide a command-line executable, and have it read and write JSON over STDIN and STDOUT. Information about the current job is passed as a JSON document to your STDIN, and you can send back status updates and completion events simply by writing JSON to your STDOUT.

When your Plugin is executed on the target server for running a job, a unique temp directory will be created for it, and any files passed to the job will be pre-downloaded for you. The CWD (current working directory) will be set to the temp dir, so your Plugin can easily list and access the input files.

Here is an example Event Plugin using Node.js:

// read job JSON from STDIN
const chunks = [];
for await (const chunk of process.stdin) chunks.push(chunk);
const job = JSON.parse( chunks.join('') );

console.log("Hello from plugin!");

// send progress updates
let percentage = 0;
setInterval( function() {
	percentage += 10;
	console.log( JSON.stringify({ xy: 1, progress: percentage / 100 }) );
	
	if (percentage >= 100) {
		console.log( JSON.stringify({ xy: 1, complete: true, code: 0 }) );
		process.exit(0);
	}
}, 1000 );

You can write Plugins using any language you want, as long as it supports reading / writing JSON over STDIO.

Event Parameters

As with most other Plugin types, you can define custom parameters for Event Plugins. These can be text fields, text boxes, code editors, select menus, checkboxes, or toolsets. The user can then fill these out when they are editing the event, and they are passed to the Plugin when a job runs. See Plugin Parameters below for more details.

Job Input

When Event Plugins are invoked via a job launching, they are passed a JSON document on STDIN (compressed onto a single line). The following top-level properties will be present in the object:

Property NameTypeDescription
xyNumberIndicates the xyOps Wire Protocol version. Will be set to 1.
typeStringThe Plugin.type, which will be set to event.
paramsObjectIf the Plugin defines any parameters, their values will be here.
(Other)VariousAll the properties from the Job object are included here.

Here is an example JSON document sent to an Event Plugin's STDIN as part of a job launch:

{
	"xy": 1,
	"type": "event",
	"targets": [
		"main"
	],
	"params": {
		"animal": "frog",
		"color": "green"
	},
	"input": {
		"data": { "foo": "bar" },
		"files": []
	},
	"fields": [],
	"limits": [],
	"actions": [],
	"notes": "",
	"category": "general",
	"plugin": "pmi11dqsxcy",
	"icon": "",
	"tags": [],
	"algo": "random",
	"username": "admin",
	"source": "user",
	"event": "emi11ejdlde",
	"id": "jmi11fqevei",
	"command": "node",
	"script": "console.log( JSON.stringify({ xy: 1, code: 0, description: \"Job successful!\" }) );\n",
	"uid": "",
	"gid": "",
	"kill": "parent",
	"env": {},
	"state": "active",
	"started": 1763256572.033,
	"now": 1763256572.024,
	"log_file_size": 0,
	"server": "smf4j79snhe",
	"groups": [
		"main"
	],
	"updated": 1763256572.033,
	"progress": 0,
	"cwd": "/opt/xyops/satellite/temp/jobs/jmi11fqevei",
	"log_file": "/opt/xyops/satellite/logs/jobs/job-jmi11fqevei.log",
	"pid": 1789701
}

See the Job structure for more details on these properties.

Note that all Plugin parameters are also passed to your Plugin process as environment variables (with IDs converted as needed).

Input Files

If your job is passed input files (either by a previous job, attached workflow node, or by manual user upload), they are made available to your Event Plugin, and file metadata is provided to you as well.

First, all job input files are automatically written out to the job's unique temp directory, which is also the current working directory for your plugin. This directory will be empty except for any input files, so you can use a glob to list them. However, the list of files is also provided to you via the Job.input object, specifically input.files. Here is an example:

"input": {
	"data": {},
	"files": [
		{
			"id": "fmktcdzp1skybhk9",
			"date": 1769321584,
			"filename": "mario.mp3",
			"size": 309425,
			"username": "admin"
		},
		{
			"id": "fmktcdzpasm25ncs",
			"date": 1769321584,
			"filename": "webb_2.jpg",
			"size": 1065694,
			"username": "admin"
		}
	]
}

So you can also discover and iterate over your input files by accessing this data structure.

Environment Variables

When Event Plugins are invoked via a job launching, they are passed a set of environment variables. These provide a convenient way to read event parameters and input data without having to parse the JSON sent to STDIN. Here is a list of the variables provided to each running job:

Variable NameDescription
XYOPSWill be set to the current xySat version.
JOB_IDWill contain the current Job.id.
JOB_NOWWill contain the current Job.now.
JOB_BASE_URLWill contain the current Job.base_url.
data_*All top-level properties from the Job.input.data object are included as environment variables, with a data_ prefix.
workflow_*All workflow parameters (user fields) are included as environment variables, with a workflow_ prefix.
workflow_data_*All top-level properties from the Job.workflowData object are included as environment variables, with a workflow_data_ prefix.
server_data_*All top-level properties from the Job.serverData object are included as environment variables, with a server_data_ prefix.
(Event Param IDs)All event parameters are passed as environment variables with the param IDs used as variable names.
(Secrets)All assigned Secret Vault variables are included as environment variables.
(Job Env)All properties from the job_env global configuration object are included as environment variables.

Job Output

Your Plugin is expected to write JSON to STDOUT in order to report status back to the xyOps primary conductor. At the very least, you need to notify xyOps that the job was completed, and the result of the job (i.e. success or fail). This is done by printing a JSON object with a xy property set to 1 (indicating the xyOps Wire Protocol version), and a code property set to 0 indicating success. You need to make sure the JSON is compacted onto a single line, and ends with a single EOL character (\n on Unix). Example:

{ "xy": 1, "code": 0 }

This tells xyOps that the job was completed successfully, and your process is about to exit. However, if the job failed and you need to report an error, you need to set the code property set to any non-zero error code you want, and add a description property set to a custom error string. Include these along with the xy property in the JSON. Example:

{ "xy": 1, "code": 999, "description": "Failed to connect to database." }

Your error code and description will be displayed on the Job Details page in the UI, and in any e-mail notifications and/or web hooks sent out for the event completion. The error code can be a number or a string.

If your Plugin writes anything other than JSON to STDOUT (or STDERR), or it is missing the xy property, it is automatically appended to your log file as text. This is so you don't have to worry about using existing code or utilities that may emit some kind of JSON output. xyOps is very forgiving in this regard.

Please note that the once you send a JSON line containing the code property, xyOps will consider your job completed, and not process any further JSON updates from your Plugin. So make sure it is the last JSON line you send for a job.

Progress

In addition to reporting success or failure at the end of a job, you can also optionally report progress at custom intervals while your job is running. This is how xyOps can display its visual progress meter in the UI, as well as calculate the estimated time remaining. To update the progress of a job, simply print a JSON document with a xy property set to 1, and a progress property, set to a number between 0.0 and 1.0. Example:

{ "xy": 1, "progress": 0.5 }

This would show progress at 50% completion, and automatically calculate the estimated time remaining based on the duration and progress so far. You can repeat this as often as you like, with as granular progress as you can provide. Note that the estimated time remaining is a "best guess effort", and is more accurate if your job progresses in a "linear" fashion, with regular progress updates.

Important

Beware of STDIO output buffering which many languages enable by default. This may delay your progress updates (not to mention other output), unless you set it to auto-flush on every write. See your specific language documentation for details.

Status

In addition to indicating job progress, you can also set a "status" string, which is displayed on the live job details page during a job run. Similar to the progress indicator, you can update this as often as you like, for e.g. to report which phase of a job you are in. To set the job status, simply print a JSON document with a xy property set to 1, and a status property set to any string you want. Example:

{ "xy": 1, "status": "Processing client report..." }

You can combine this with progress (and any other job updates too):

{ "xy": 1, "progress": 0.5, "status": "Processing file 34 of 68..." }

Note that the status string is only displayed during a live job run, and not after the job completes. It is shown just under the progress bar, replacing the summary box title heading.

Perf Metrics

You can optionally include performance metrics at the end of a job, which are displayed as a pie chart on the Job Details page. These metrics can consist of any categories you like, and the JSON format is a simple perf object where the values represent the amount of time spent in seconds. Example:

{ "xy": 1, "perf": { "db": 18.51, "http": 3.22, "gzip": 0.84 } }

The perf keys can be anything you want. They are just arbitrary categories you can make up, which represent how your Plugin spent its time during the job.

xyOps accepts a number of different formats for the perf metrics, to accommodate various performance tracking libraries. For example, you can provide the metrics in query string format, like this:

{ "xy":1, "perf": "db=18.51&http=3.22&gzip=0.84" }

If your metrics include a total (or t) in addition to other metrics, this is assumed to represent the total time, and will automatically be excluded from the pie chart (but included in the performance history graph).

If you track metrics in units other than seconds, you can provide the scale. For example, if your metrics are all in milliseconds, just set the scale property to 1000. Example:

{ "xy": 1, "perf": { "scale": 1000, "db": 1851, "http": 3220, "gzip": 840 } }

The slightly more complex format produced by our own pixl-perf library is also supported.

Custom Content

If your Plugin produces statistics or other tabular data, you can have xyOps render this into a table on the Job Details page. You can do this during or at the end of a job run. Simply print a JSON object with a property named table, containing the following keys:

Property NameDescription
titleOptional title displayed above the table, defaults to "Job Data Table".
headerOptional array of header columns, displayed in shaded bold above the main data rows.
rowsRequired array of rows, with each one being its own inner array of column values.
captionOptional caption to show under the table (centered, small gray text).

Here is an example data table. Note that this has been expanded for documentation purposes, but in practice your JSON needs to be compacted onto a single line when printed to STDOUT.

{
	"xy": 1,
	"table": {
		"title": "Sample Job Stats",
		"header": [
			"IP Address", "DNS Lookup", "Flag", "Count", "Percentage"
		],
		"rows": [
			["62.121.210.2", "directing.com", "MaxEvents-ImpsUserHour-DMZ", 138, "0.0032%" ],
			["97.247.105.50", "hsd2.nm.comcast.net", "MaxEvents-ImpsUserHour-ILUA", 84, "0.0019%" ],
			["21.153.110.51", "grandnetworks.net", "InvalidIP-Basic", 20, "0.00046%" ],
			["95.224.240.69", "hsd6.mi.comcast.net", "MaxEvents-ImpsUserHour-NM", 19, "0.00044%" ],
			["72.129.60.245", "hsd6.nm.comcast.net", "InvalidCat-Domestic", 17, "0.00039%" ],
			["21.239.78.116", "cable.mindsprung.com", "InvalidDog-Exotic", 15, "0.00037%" ],
			["172.24.147.27", "cliento.mchsi.com", "MaxEvents-ClicksPer", 14, "0.00035%" ],
			["60.203.211.33", "rgv.res.com", "InvalidFrog-Croak", 14, "0.00030%" ],
			["24.8.8.129", "dsl.att.com", "Pizza-Hawaiian", 12, "0.00025%" ],
			["255.255.1.1", "favoriteisp.com", "Random-Data", 10, "0%" ]
		],
		"caption": "This is an example stats table you can generate from within your Plugin code."
	}
}

If you would prefer to generate your own custom HTML content from your Plugin code, and just have it rendered into the Job Details page, you can do that as well. Simply print a JSON object with a property named html, containing the following keys:

Property NameDescription
titleOptional title displayed above the section, defaults to "Job Custom Data".
contentRequired Raw HTML content to render into the page.
captionOptional caption to show under your HTML (centered, small gray text).

Here is an example HTML report. Note that this has been expanded for documentation purposes, but in practice your JSON needs to be compacted onto a single line when printed to STDOUT.

{
	"xy": 1,
	"html": {
		"title": "Sample Job Report",
		"content": "This is <b>HTML</b> so you can use <i>styling</i> and such.",
		"caption": "This is a caption displayed under your HTML content."
	}
}

Note that only basic HTML elements are allowed here, in order to prevent XSS attacks. See sanitize_html_config in the /opt/xyops/internal/ui.json file for the full list of allowed tags.

If your Plugin generates plain text instead of HTML, you can change html to text, which will preserve formatting such as whitespace. Example:

{
	"xy": 1,
	"text": {
		"title": "Sample Text Report",
		"content": "This is plain text, so no styling allowed here.",
		"caption": "This is a caption displayed under your text content."
	}
}

Similarly, if your Plugin generates markdown, you can include that instead of HTML or text:

{
	"xy": 1,
	"markdown": {
		"title": "Sample Markdown Report",
		"content": "This is **Markdown** so you can use *styling*, [links](https://xyops.io) and such.",
		"caption": "This is a caption displayed under your Markdown content."
	}
}

Note that only one of html, text or markdown output is allowed per job (text and markdown are rendered down to HTML).

Job Labels

Your can optionally add custom labels to your jobs, which will be displayed on the completed job history pages alongside the Job IDs. This is useful if you launch jobs with custom parameters, and need to differentiate them in the completed list.

To set the label for a job, simply include a label property in your Plugin's JSON output, set to any string you want. Example:

{ "xy": 1, "label": "Reindex Database" }

This would cause the "Reindex Database" label to be displayed alongside the Job ID.

Output Data

To include arbitrary data output from your job, which will be automatically passed to the next job (via workflow node connection or run event action), use this message format:

{
	"xy": 1,
	"data": {
		"text": "This is some sample data to pass to the next job!",
		"hostname": "raspberrypi",
		"pid": 13094,
		"random": 0.54,
		"obj": { "foo": 1, "bar": null, "bool": true }
	}
}

The format of the data object is freeform, and can contain whatever content you want. Note that the above example is pretty-printed for display, but in practice all messages must be sent as single lines, so remember to compact your JSON when serializing it.

Note that if you send multiple messages containing data within the same job, the top-level data object properties are shallow-merged (the latter prevails on duplicate keys). Using this you can add data incrementally during a job run. Additionally, if you are overwriting a top-level array with another array, it will be concatenated instead of replaced. Example:

{ "xy": 1, "data": { "arr": [0, 1, 2] } }

Then later, in the same job:

{ "xy": 1, "data": { "arr": [3, 4, 5] } }

This would end up with [0, 1, 2, 3, 4, 5] in the final arr data array when the job completes.

Output Files

To upload files as part of your job output, you can simply tell xyOps where they are on disk. When your job completes, the files will be attached and uploaded with the job data, and displayed in the UI. They will also be passed to the next job if applicable (via workflow node connection or run event action). Here is an example:

{
	"xy": 1,
	"files": [
		"/path/to/file1.txt",
		"/path/to/file2.mp4"
	]
}

You don't actually have to name each file. You can instead specify a wildcard (glob pattern) which may match multiple files:

{
	"xy": 1,
	"files": [ "/path/to/*.mp4" ]
}

If the files are located in the current working directory (your job's unique temp directory), you can omit the leading path and just include filename(s):

{
	"xy": 1,
	"files": [ "*.mp4" ]
}

If you want to have xyOps delete the files for you after uploading, specify an object inside the files array, with path and delete properties. Example:

{
	"xy": 1,
	"files": [
		{ "path": "*.mp4", "delete": true }
	]
}

Note that if you send multiple messages with files properties, the previous list is overwritten (i.e. the latter prevails).

Job Tags

To add tags to the current job, use the following "push" message format:

{
	"xy": 1,
	"push": {
		"tags": ["tag1", "tag2"]
	}
}

The push object is used here to instruct xyOps to "push" (append) tags onto the existing set (you cannot replace or delete tags). The tags themselves should be valid Tag.ids, and duplicates are automatically removed.

Job Actions

To add actions to the current job, use the following "push" message format. This example would send an email to a specific address when the job completes:

{
	"xy": 1,
	"push": {
		"actions": [
			{ "condition": "complete", "type": "email", "email": "admin@mycompany.com", "users": [], "enabled": true }
		]
	}
}

Here is another example which will launch a subsequent job when the current job completes successfully:

{
	"xy": 1,
	"push": {
		"actions": [
			{ "condition": "success", "type": "run_event", "event_id": "emi2d3f42zy", "params": {}, "enabled": true }
		]
	}
}

And if you want the action to run instantly (i.e. do not wait for the job to complete), use the special instant condition, like this:

{
	"xy": 1,
	"push": {
		"actions": [
			{ "condition": "instant", "type": "channel", "channel_id": "sev1", "enabled": true }
		]
	}
}

The push object is used here to instruct xyOps to "push" (append) actions onto the existing set (you cannot replace or delete actions). See Action Types for all the possible action objects you can add here.

Server Data

To update the Server User Data for the current server from inside a running job, use the following output format:

{
	"xy": 1,
	"serverData": {
		"foo": "bar"
	}
}

Note that the server data is shallow-merged, so you can specify a sparsely-populated object and it will only add / replace the included top-level properties. If your job outputs multiple messages with serverData they are all shallow-merged together (the latter prevails on duplicate keys).

The server user data is only updated when the job completes. If you need to update the server data immediately during a job, use the update_server_data API instead.

Workflow Data

To update the Workflow Data for the current workflow from inside a running job, use the following output format:

{
	"xy": 1,
	"workflowData": {
		"foo": "bar"
	}
}

Note that the workflow data is shallow-merged, so you can specify a sparsely-populated object and it will only add / replace the included top-level properties. If your job outputs multiple messages with workflowData they are all shallow-merged together. Additionally, top-level arrays are concatenated when merging.

The workflow data is only updated in the parent workflow when the sub-job completes.

Action Plugins

Action Plugins are designed for custom actions that take place in response to jobs starting, completing, or completing with specific result codes (e.g. success, error, warning, critical, etc.). They can also run in response to alerts firing or clearing. You can already assign a number of built-in actions including sending an email, firing a web hook, launching an event, taking a server snapshot, and more. But with Plugins you can write your own actions that do anything you want. They can even be configured to accept a custom set of parameters that are configured by the user in the UI.

Action Plugins run on the primary conductor server, as they are part of the core engine. However, you can still write them in any language, as they are spawned as a child subprocess, and communication API is JSON over STDIO. To create an Action Plugin, navigate to the Plugins page, and click the New Plugin button. For the Plugin type, select "Action Plugin".

Action Parameters

As with most other Plugin types, you can define custom parameters for Action Plugins. These can be text fields, text boxes, code editors, select menus, checkboxes, or toolsets. The user can then fill these out when they are editing the event or alert, and they are passed to the Plugin when the action fires. See Plugin Parameters below for more details.

Action Input

When Action Plugins are invoked, they are passed a JSON document on STDIN (compressed to a single line). The following top-level properties will be present in the object:

Property NameTypeDescription
xyNumberIndicates the xyOps Wire Protocol version. Will be set to 1.
typeStringThe Plugin.type, which will be set to action.
conditionStringThe Action.condition which activated the Plugin.
paramsObjectIf the Plugin defines any parameters, their values will be here.
secretsObjectIf the Plugin is assigned any Secrets, they are included here (as well as in environment variables).
base_urlStringA localhost base URL is provided in case your Plugin needs to make any xyOps API calls.
(Other)VariousBased on context; see below.

If the Action Plugin is being invoked in job-related context (i.e. on job start, job complete, or other job actions) the contents of JobHookData will also be merged in at the top-level. Similarly, if the plugin is being invoked in an alert-related context (alert fired or cleared), then the contents of AlertHookData will be merged in.

Here is an example JSON document sent to an Action Plugin's STDIN as part of a job completion:

{
	"xy": 1,
	"type": "action",
	"condition": "success",
	"params": {
		"foo": "Baz"
	},
	"secrets": {
		"DB_USER": "dev",
		"DB_PASS": "1234"
	},
	"base_url": "http://localhost:5522",
	"job": {
		"id": "jmhzaot10tm",
		"complete": true,
		"code": 0,
		"description": "",
		"completed": 1763151180.219,
		"elapsed": 0.701
		/* See Job data structure for more */
	},
	"action": {
		"type": "plugin",
		"condition": "success",
		"plugin_id": "pmhzan6voso",
		/* See Action data structure for more */
	},
	"event": {
		"id": "emhzaoispta",
		/* See Event data structure for more */
	},
	"plugin": {
		"id": "shellplug",
		/* See Plugin data structure for more */
	},
	"category": {
		"id": "general",
		/* See Category data structure for more */
	},
	"server": {
		"id": "smf4j79snhe",
		/* See Server data structure for more */
	},
	"nice_server": "raspberrypi",
	"nice_hostname": "raspberrypi",
	"links": {
		"job_details": "https://local.xyops.io:5523/#Job?id=jmhzaot10tm",
		"job_log": "https://local.xyops.io:5523/api/app/download_job_log?id=jmhzaot10tm&t=lnJY9P2-VTuqNIlV7jReuw",
		"job_files": "(None)"
	},
	"display": {
		"elapsed": "0 seconds",
		"log_size": "23 bytes",
		"perf": "(No metrics provided)",
		"mem": "47.4 MB (Peak: 47.4 MB)",
		"cpu": "28% (Peak: 28%)"
	},
	"text": "xyOps Job completed successfully on raspberrypi: Run Custom Action: https://local.xyops.io:5523/#Job?id=jmhzaot10tm"
}

See JobHookData for more details on these properties.

And here is an example JSON document sent to an Action Plugin's STDIN as part of a new alert triggering:

{
	"xy": 1,
	"type": "action",
	"condition": "alert_new",
	"alert_def": {
		"id": "active_jobs_high",
		/* See Alert data structure for more */
	},
	"params": {
		"foo": "Foosball"
	},
	"secrets": {},
	"base_url": "http://localhost:5522",
	"server": {
		"id": "smf4j79snhe",
		/* See Server data structure for more */
	},
	"alert": {
		"id": "amhzbmb6jhw",
		"exp": "monitors.active_jobs >= 1",
		"message": "Active job count is too high: 1",
		/* See AlertInvocation data structure for more */
	},
	"active_jobs": [
		{
			"id": "jmhzblxlvhl",
			"event": "emfetc6wcpw"
		}
	],
	"date_time": "Fri Nov 14 2025 12:39:02 GMT-0800 (Pacific Standard Time)",
	"nice_groups": "Main Group",
	"nice_load_avg": 0.02,
	"nice_mem_total": "906.2 MB",
	"nice_mem_avail": "624.4 MB",
	"nice_uptime": "91 days",
	"nice_cpu": "Sony UK BCM2837 (arm64)",
	"nice_os": "Debian GNU/Linux 12",
	"nice_notes": "(None)",
	"nice_hostname": "raspberrypi",
	"nice_ip": "10.1.10.92",
	"nice_server": "raspberrypi",
	"nice_active_jobs": "- [Job #jmhzblxlvhl](https://local.xyops.io:5523/#Job?id=jmhzblxlvhl) (Convert Video Format)\n",
	"links": {
		"server_url": "https://local.xyops.io:5523/#Server?id=smf4j79snhe",
		"alert_url": "https://local.xyops.io:5523/#Alerts?id=amhzbmb6jhw"
	},
	"text": "xyOps Alert: raspberrypi: High Active Jobs: n/a: https://local.xyops.io:5523/#Alerts?id=amhzbmb6jhw"
}

See AlertHookData for more details on these properties.

Action Output

When your Action Plugin has completed, you can inform xyOps of the result (success or fail), and any additional details you might want to add. This is done by sending a JSON record out through your process STDOUT. Similar to the document you received via STDIN, it needs to have a top-level xy property set to 1, a code property indicating success or fail, and an optional description property:

{
	"xy": 1,
	"code": 0,
	"description": "Action success!"
}

As with all other xyOps APIs, a code of 0 or false indicates success, while any other value means that an error occurred. You can use the description property to pass an optional success or error message. All this information will be stored with the job or alert, and displayed in the xyOps UI.

As an advanced tip, you can also include an optional details property, which is rendered as Markdown in the details dialog for the action. This can be useful if your action produces a large amount of output or logs that you want to capture and expose to the user.

If your Plugin does not output JSON, no problem. When no JSON is detected in the output stream, xyOps will assume success or failure based on the process exit code, and display the raw output as plain text, if any.

Trigger Plugins

Trigger Plugins are extensions of the scheduler system, in that they can decide "when" and "if" to launch jobs. Specifically, if an event uses a trigger plugin, it is consulted once per scheduled job, and the Plugin decides whether to launch each assigned job or not. For example, this can be used for custom timing algorithms like sunrise / sunset, or even watching a directory or S3 prefix for new files to appear.

This is a "modifier" trigger, so it needs to be configured in conjunction with a standard schedule trigger. The schedule sets the cadence and frequency of when the Plugin is launched.

Trigger Plugins run on the primary conductor server, as they execute before a job is launched and before a server is chosen for it. However, like the other Plugin types, they are spawned as sub-processes and can be written in virtually any language. There is no SDK to use -- xyOps communicates with Plugins via simple JSON over STDIO.

To create a Trigger Plugin, navigate to the Plugins page, and click the New Plugin button. For the Plugin type, select "Trigger Plugin".

Trigger Parameters

As with most other Plugin types, you can define custom parameters for Trigger Plugins. These can be text fields, text boxes, code editors, select menus or checkboxes. The user can then fill these out when they are editing the event, and they are passed to the Plugin when deciding to run jobs for that event.

Trigger Input

When your trigger plugin is invoked, it will be passed an array of all the events awaiting a launch decision (i.e. all the events which have added your trigger plugin to them). A single line of JSON will be passed to your plugin process via STDIN, which looks like this (pretty-printed for display purposes):

{
	"xy": 1,
	"type": "trigger",
	"items": [
		{
			"timezone": "America/Los_Angeles", 
			"now": 1757642510, 
			"dargs": {
				"year": 2022, 
				"month": 11, 
				"day": 29, 
				"rday": -3,
				"weekday": 2, 
				"hour": 22, 
				"minute": 29
			}, 
			"params": {
				"longitude": -118.2437,
				"latitude": 34.0522
			}, 
			"job": {
				"id": "emdy0mg1oum",
				"title": "Convert Video Format",
				"enabled": true,
				"username": "admin",
				"modified": 1726463348,
				"created": 1726463348,
				"category": "cat2",
				"targets": [
					"main"
				],
				"algo": "random",
				"notes": ""
			}
		}
	],
	"secrets": {},
	"active_jobs": [],
	"base_url": "http://localhost:5522"
}

As with all xyOps STDIO communication, the JSON will always have a top-level xy property set to 1 (the xyOps Wire Protocol version), and a type property set to trigger. Here is the full list of top-level properties you can expect:

Property NameTypeDescription
xyNumberThe xyOps Wire Protocol version.
typeStringWill always be set to triger for Trigger Plugin payloads.
itemsArrayAn array of scheduled events for the plugin to process. See below for details.
secretsObjectIf your plugin was assigned any secrets, they will be passed in this object (and also as environment variables).
active_jobsArrayAn array of all active Jobs currently running.
base_urlStringA localhost base URL is provided in case your Plugin needs to make any xyOps API calls.

The items array will contain an element for each event that is scheduled to launch, and has the plugin assigned as a trigger. It is up to your plugin code to decide if each event should actually launch a job or not. You are also provided some other information about the each event:

Property NameTypeDescription
timezoneStringThe currently selected timezone for the event.
nowNumberThe current time for the potential job launch in Epoch seconds. Note that this may be in the past, if xyOps is catching up on missed events.
dargsObjectThe current date/tme for the job launch, separated out into individual numerical elements, in the event's timezone. See below for details.
paramsObjectThis object will contain your plugin's own custom defined parameters, filled out by the user at the event level.
jobObjectThis is a copy of the Event object that will be used to launch the job if your plugin decides it should.

Here are descriptions of all the dargs date/time properties:

Property NameTypeDescription
yearNumberThe year as an integer, e.g. 2025.
monthNumberThe month number from 1 to 12.
dayNumberThe month day from 1 to 31.
rdayNumberThe reverse month day (e.g. the last day of the month will be -1, the second-to-last day will be -2, and so on).
weekdayNumberA number representing the day of the week, from 0 (Sunday) to 6 (Saturday).
hourNumberThe hour in 24-hour format (from 0 to 23).
minuteNumberThe minute number from 0 to 59.

The JSON will be provided to your plugin as a single line on STDIN. You will need to read and parse the JSON to iterate over the items array. Here is an example in Node.js (but you can use any language you want):

// read JSON from STDIN
const chunks = [];
for await (const chunk of process.stdin) { chunks.push(chunk); }
const data = JSON.parse( chunks.join('') );

data.items.forEach( function(item) {
	// do something with item...
} );

Trigger Output

Once your plugin decides which events should launch jobs (if any), you need to communicate that information back to xyOps. This is done by sending a JSON record out through your process STDOUT. Similar to the document you received via STDIN, it needs to have a top-level xy property set to 1, and an items array:

{
	"xy": 1,
	"items": [ false, false, true ]
}

The items array should have the same number of elements as the initial one you received, and each element can be set to a simple Boolean as shown above. In this case true means launch a job for the event, and false means do not. Each item in your output array needs to line up with the corresponding object in the input array, via their indexes.

Now, instead of a simple Boolean, the items can also be objects containing a launch Boolean (indicating whether to launch a job or not). This alternate verbose format exists so you can include additional metadata for the launched jobs. Example:

{
	"xy": 1,
	"items": [ 
		{
			"launch": false
		},
		{
			"launch": false
		},
		{
			"launch": true,
			"data": { "mykey1": "myvalue1" },
			"files": [ "/path/to/file.txt" ]
		}
	]
}

In fact, what you can do instead of constructing a new items array for the output, is to modify in place the existing items array you received via STDIN (i.e. just add launch and other properties directly to it), and then echo the modified object back out via STDOUT. To illustrate this, here is a silly example that randomly launches jobs based on a 50% probability:

// read JSON from STDIN
const chunks = [];
for await (const chunk of process.stdin) { chunks.push(chunk); }
const data = JSON.parse( chunks.join('') );

data.items.forEach( function(item) {
	// randomly launch a job or not (silly example)
	item.launch = Math.random() < 0.5;
} );

// write to STDOUT
process.stdout.write( JSON.stringify(data) + "\n" );

Obviously your plugin will do something more useful than this, but you get the idea. See the following sections to learn what else you can include in the items array elements.

Trigger Data

When your trigger plugin decides to launch a job, you can optionally include arbitrary data that will be passed to it. This is done by including a data object inside the item element, alongside the launch boolean. Example:

{
	"xy": 1,
	"items": [ 
		{
			"launch": false
		},
		{
			"launch": false
		},
		{
			"launch": true,
			"data": { 
				"mykey1": "myvalue1",
				"mykey2": "myvalue2"
			}
		}
	]
}

The format of the data property is user-defined, and it will be passed verbatim to the launched job, becoming the input.data property inside the Job object (same as if data is passed to it from a previous chained job, workflow, action, etc.).

Trigger Files

You can also send along files to your launched jobs. These will be attached to the job as inputs, and automatically downloaded in each job's temp directory on the remote server. To do this, include a files array alongside your launch property. The files array should be populated like this:

{
	"xy": 1,
	"items": [ 
		{
			"launch": false
		},
		{
			"launch": false
		},
		{
			"launch": true,
			"files": [
				{ "path": "/path/to/file.jpg", "delete": true }
			]
		}
	]
}

Each object in the files array needs to have a path property that points to a single file. You can also optionally pass a delete property. If this is set to true then xyOps will automatically delete the file after it is uploaded.

This mechanism works the same as if the files were passed to your job from a previous chained job, workflow, action, etc.

Trigger Delay

If you would like to delay a job launch, send back a delay property alongside the launch Boolean, set to the number of seconds you want the job to wait before running. Example:

{
	"xy": 1,
	"items": [ 
		{
			"launch": false
		},
		{
			"launch": false
		},
		{
			"launch": true,
			"delay": 30
		}
	]
}

Note that this mechanism works similarly to the built-in Delay scheduler option. Meaning, the job still "launches" but is set to a special pending state until the specified delay elapses, at which time the job becomes active and runs proper. Also note that the delay value is computed relative to the job's original start time (i.e. the Job.now actual on-the-minute time).

Monitor Plugins

Monitor Plugins can extend the xyOps monitoring system by gathering any custom metrics that you want. These Plugins run directly on the servers they are targeted to (i.e. xySat runs them as child processes), and then their custom metrics are included along with the server last-minute monitoring data sent back to the xyOps conductor server.

Instead of running in response to an event or action, Monitor Plugins run every single minute, 24x7. They are essentially "data collectors", and are expected to produce monitoring data for the instant in which they are run, or in many cases they return accumulated data for the last 60 seconds.

Monitor Plugins differ from the other xyOps Plugin types in that they are not passed a JSON document on STDIN, and they do not need to produce a specific JSON output format. The API contract for these plugins is simpler -- they just execute every minute, and they can output freeform JSON, XML, or plain text. It is then up to your Monitors to pull specific data values out of that data, and use them for graphs, alerts, etc.

Here is an example. This Plugin actually ships with xyOps, and it tracks the total number of open files on the server:

  • Plugin Title: Count Open Files
  • Plugin ID: open_files
  • Command: /bin/sh
  • Script: cat /proc/sys/fs/file-nr
  • Format: text

That's it -- that's the entire Plugin, including the source code. In this example the code is this small /bin/sh shell script:

cat /proc/sys/fs/file-nr

The output of this is obviously just plain text:

1056	0	9223372036854775807

But that's fine! The syntax doesn't matter at this point. What happens is, this raw data gets included with the server's ServerMonitorData.commands, keyed by the Plugin.id, and is then made available to monitors and alerts in this format:

"commands": {
	"open_files": "1056\t0\t9223372036854775807"
}

Then separately, we define a Monitor which pulls the appropriate value (in this case the first number) out of the raw text:

  • Monitor Title: Open Files
  • Expression: commands.open_files
  • Data Match: (\\d+)
  • Data Type: integer

And it's as simple as that. Our custom monitor now graphs the total open files on the server over time, based on a custom command we execute.

If your Monitor Plugin is set to XML or JSON format, you can actually output a large, multi-value data structure, and different monitors can grab specific values out of it. This is really useful for things like grabbing all of your application's performance metrics in one command, output it as a large JSON/XML structure, and then you can configure individual xyOps monitors to pull out and graph specific values. Alerts can trigger on the data values as well.

Plugin Parameters

Most Plugins accept one or more "parameters", which are configurable user fields. These are displayed in the UI for users to populate when they are configuring events or workflows. See below for all the types of parameters available. See Plugin.params for the internal data structure.

Each parameter is stored as an object inside the Plugin's params array. Every control type needs a locally unique id, a user-facing title, and a type. Most controls also have a default value, plus optional caption, required, regex and locked properties where supported.

Text

A "text" parameter type is presented to the user as a single-line text field.

An optional "variant" property may be included, which changes the visible UI control in the browser. Internally, this is rendered as a native HTML input using type="VARIANT", so the browser provides the exact picker, keyboard, validation and display behavior. Here are the supported variants:

VariantBrowser UI
colorA native color picker, rendered as <input type="color">.
dateA native date picker, rendered as <input type="date">.
datetime-localA native local date and time picker, rendered as <input type="datetime-local">.
emailA single-line email field with browser email validation, rendered as <input type="email">.
numberA numeric input, usually with spinner controls and numeric validation, rendered as <input type="number">.
passwordA visually masked single-line field, rendered as <input type="password">.
textA standard single-line text field, rendered as <input type="text">.
timeA native time picker, rendered as <input type="time">.
telA telephone-style text field, usually with a phone keypad on mobile browsers, rendered as <input type="tel">.
urlA single-line URL field with browser URL validation, rendered as <input type="url">.

Note that the parameter value is almost always set to a string -- the "variant" only controls the visual UI control and behavior. However, the number variant is a special case, where the value will actually be parsed and stored in the parameters as an actual JavaScript Number, or null when empty.

The number variant is also special in that you can specify a range property (string), which limits the minimum, maximum, and step increment for the value. The range should be in the format: MIN - MAX / STEP. So for example, to limit the number range from 0 to 100 with increments of 5, use 0 - 100 / 5. Floats and negatives are allowed, and the step can be the special keyword any (for no enforced step increment).

Example text parameter definition:

{
	"id": "filename",
	"title": "Filename",
	"type": "text",
	"variant": "text",
	"value": "",
	"caption": "Enter the target filename.",
	"required": true,
	"regex": "^[\\w\\-.]+$"
}

Example number variant definition:

{
	"id": "timeout",
	"title": "Timeout",
	"type": "text",
	"variant": "number",
	"value": 30,
	"range": "1 - 300 / 1",
	"caption": "Enter the timeout in seconds.",
	"required": true
}

Note

The password text variant only masks the value while the user is entering it, which can help protect against prying eyes and shoulder surfing. For example, it can be useful on a Magic Link form. It does not encrypt the value or store it as a secret. After submission, the value becomes a regular plain-text parameter on the event or job, and any xyOps user with access to the event or category can easily retrieve it. Event and job detail screens initially hide the value behind a "Click to View" link, but this is only a visual convenience. Use a Secret Vault for sensitive values that require actual protection.

Textarea

A "textarea" parameter type is presented to the user as a multi-line text box. Here the user can enter multiple lines of text (no maximum length is enforced).

Example textarea parameter definition:

{
	"id": "message",
	"title": "Message",
	"type": "textarea",
	"value": "",
	"caption": "Enter the message body.",
	"required": true,
	"regex": ".+"
}

Code

A "code" parameter type is a variant of the textarea, but it is presented to the user as a button that pops up a full code editor dialog. The user can enter "code" of any language, and the format is automatically detected and syntax-highlighted.

Example code parameter definition:

{
	"id": "script",
	"title": "Script Source",
	"type": "code",
	"value": "#!/bin/sh\n\n# Enter your shell script code here\n",
	"caption": "Enter the script source to execute.",
	"required": true
}

JSON

A "JSON" parameter type is a variant of the textarea, but it is presented to the user as a button that pops up a full code editor dialog with JSON syntax-highlighting and line numbers. The JSON is also validated, so the user can only enter a proper JSON document.

This type is special in that the JSON is parsed and stored in the parameters as a real object (not a string).

Example JSON parameter definition:

{
	"id": "headers",
	"title": "HTTP Headers",
	"type": "json",
	"value": {
		"Content-Type": "application/json"
	},
	"caption": "Enter custom request headers as a JSON object."
}

A "menu" is presented as a drop-down menu, with a configurable list of items. The plugin declares these as a CSV list. Example:

Alpha, Beta, Gamma

This item has type select in the API, to match the HTML element of the same name.

To include an empty item at the top of the menu (allowing the user to select "nothing" as an option), simply start the CSV list with a leading comma. Example:

, Alpha, Beta, Gamma

To set the item values and the labels separately, specify the values in square brackets like this:

Alpha [a1], Beta [b2], Gamma [c3]

This would show only the labels in the menu ("Alpha", "Beta", "Gamma"), but in the data the values would be specified instead (a1, b2, c3). Note that the values may only contain alphanumerics, underscores, dashes and dots, and when this feature is used the visual labels are not passed into the data at all.

Note that if you check the "Multi-Select" checkbox when configuring the menu field, your parameter value will be an array of selected values, as opposed to a string for a single-select menu.

Example single-select menu parameter definition:

{
	"id": "environment",
	"title": "Environment",
	"type": "select",
	"value": "Development [dev], Staging [stage], Production [prod]",
	"caption": "Select the target environment.",
	"multiple": false
}

Example multi-select menu parameter definition:

{
	"id": "regions",
	"title": "Regions",
	"type": "select",
	"value": "US East [us-east], US West [us-west], Europe [eu]",
	"caption": "Select one or more deployment regions.",
	"multiple": true
}

Bucket Menu

A "bucket menu" is a dynamically populated menu, which automatically loads its items from a global Storage Bucket that you configure. Using this feature you can have menus across multiple Plugins or events that all share the same item pool.

To define the set of items, simply create a Storage Bucket, and edit the JSON data within. Place your JSON array anywhere in the bucket data. Example:

{
	"countries": [
		"United States",
		"Canada",
		"Mexico",
		"Brazil",
		"United Kingdom",
		"France",
		"Germany",
		"Japan",
		"Australia",
		"South Africa"
	]
}

Make sure to save the bucket changes after editing the JSON.

Then, when you add your parameter and select the "Bucket Menu" field type, you will need to select the target bucket, and optionally enter a "Data Path". The path is how to specify where inside the storage bucket your item array lives. In the above example it's in a top-level property named countries, so that's exactly what you'd enter for the Data Path.

This allows you to store multiple different item lists in the same storage bucket.

Alternatively, if your item array lives at the very top level of the bucket JSON data, i.e. like this:

[
	"United States",
	"Canada",
	"Mexico",
	"Brazil",
	"United Kingdom",
	"France",
	"Germany",
	"Japan",
	"Australia",
	"South Africa"
]

Then you should leave the "Data Path" field empty (as in this case the entire bucket data is the array).

This feature also allows you to customize the menu item "values" (i.e. what goes into the Job.params) and menu item "labels" (i.e. what is displayed in the menu) separately. To do this, define your JSON array as an array of objects, with each object containing an id and a title property. Example:

{
	"countries": [
		{ "id": "US", "title": "United States" },
		{ "id": "CA", "title": "Canada" },
		{ "id": "MX", "title": "Mexico" },
		{ "id": "BR", "title": "Brazil" },
		{ "id": "GB", "title": "United Kingdom" },
		{ "id": "FR", "title": "France" },
		{ "id": "DE", "title": "Germany" },
		{ "id": "JP", "title": "Japan" },
		{ "id": "AU", "title": "Australia" },
		{ "id": "ZA", "title": "South Africa" }
	]
}

So in this case if the user selected "Germany" from the menu, the actual job param value would be the string DE.

Finally, you can define groups of items in the menu by including an object with a label and an items sub-array. These show up as delimited labeled sections within the menu (a.k.a. an optgroup). Example of this:

{
	"countries": [
		{
			"label": "Americas",
			"items": [
				{ "id": "US", "title": "United States" },
				{ "id": "CA", "title": "Canada" },
				{ "id": "MX", "title": "Mexico" },
				{ "id": "BR", "title": "Brazil" }
			]
		},
		{
			"label": "Europe",
			"items": [
				{ "id": "GB", "title": "United Kingdom" },
				{ "id": "FR", "title": "France" },
				{ "id": "DE", "title": "Germany" }
			]
		},
		{
			"label": "Asia / Other",
			"items": [
				{ "id": "JP", "title": "Japan" },
				{ "id": "AU", "title": "Australia" },
				{ "id": "ZA", "title": "South Africa" }
			]
		}
	]
}

Note that if you check the "Multi-Select" checkbox when configuring the bucket menu field, your parameter value will be an array of selected values, as opposed to a string for a single-select menu.

Example bucket menu parameter definition:

{
	"id": "country",
	"title": "Country",
	"type": "bucket",
	"bucket_id": "countries",
	"bucket_path": "countries",
	"caption": "Select the country to process.",
	"multiple": false
}

If the item array is at the top level of the bucket data, set bucket_path to an empty string:

{
	"id": "countries",
	"title": "Countries",
	"type": "bucket",
	"bucket_id": "countries",
	"bucket_path": "",
	"caption": "Select one or more countries to process.",
	"multiple": true
}

System Menu

A "system menu" is a dynamically populated menu, similar to a Bucket Menu, but the menu items are pulled from xyOps itself. This is useful when a Plugin needs the user to select an existing xyOps object, such as an event, category, server, server group, plugin, user, role, web hook or monitor.

When you add your parameter and select the "System Menu" field type, you will need to select the internal xyOps list to use for the menu. The menu is populated automatically from the current system data, and xyOps adds a (None) item at the top so no item is selected by default.

When the user selects an item, the selected item's ID is stored in Job.params and passed to the Plugin. For example, if you define a parameter with ID custom_event, point it at the Events system list, and the user selects an event with ID emp6dulft42zjevn8, the Plugin would receive this:

{
	"custom_event": "emp6dulft42zjevn8"
}

Most system menus store the selected item's normal id property. The Users menu is a special case, and stores the selected user's username.

The following system lists are available:

ListStored Value
AlertsAlert ID
AlgorithmsTarget algorithm ID
BucketsBucket ID
CategoriesCategory ID
ChannelsChannel ID
EventsEvent ID
GroupsServer group ID
MonitorsMonitor ID
PluginsPlugin ID
RolesRole ID
ServersServer ID
TagsTag ID
TargetsServer group ID or server ID
UsersUsername
Web HooksWeb Hook ID

The "Targets" menu is a combined list which includes both server groups and individual servers, arranged into menu sections. This is handy for Plugin parameters that should accept either kind of job target. The Algorithms menu contains the built-in event target selection algorithms, such as Random.

API Keys and Secrets are intentionally not offered as system menu sources.

Note that if you check the "Multi-Select" checkbox when configuring the system menu field, your parameter value will be an array of selected values, as opposed to a string for a single-select menu.

Example system menu parameter definition:

{
	"id": "target_event",
	"title": "Target Event",
	"type": "system",
	"list_id": "events",
	"caption": "Select an event from the system.",
	"multiple": false
}

Example multi-select system menu parameter definition:

{
	"id": "notify_users",
	"title": "Notify Users",
	"type": "system",
	"list_id": "users",
	"caption": "Select one or more users to notify.",
	"multiple": true
}

Checkbox

A checkbox is displayed with a label, and the "checked" state is stored as a Boolean parameter value (true or false).

Example checkbox parameter definition:

{
	"id": "dry_run",
	"title": "Dry Run",
	"type": "checkbox",
	"value": true,
	"caption": "Preview changes without applying them."
}

Hidden

A hidden type is not shown in the UI. Instead, it's just a hidden, pre-populated key/value pair that is passed to the Plugin as a parameter. The value is specified when the hidden field is added.

Example hidden parameter definition:

{
	"id": "api_version",
	"title": "API Version",
	"type": "hidden",
	"value": "v2"
}

Toolset

Arguably the most powerful of the Plugin parameter types, the "toolset" is presented as a drop-down menu, with a dynamic set of sub-parameters that appear based on the menu selection (i.e. the "tool"). In this way you can request different parameters from the user based on whichever "tool" is selected.

The toolset "data" is entered in JSON format, and describes all the tools and sub-parameters that should be displayed for each tool. Here is an example:

{
	"id": "s3_action",
	"title": "S3 Action",
	"type": "toolset",
	"caption": "Select which S3 operation to perform.",
	"data": {
		"default": "uploadFiles",
		"tools": [
			{
				"id": "uploadFiles",
				"title": "Upload Files",
				"description": "Upload local files to S3",
				"fields": [
					{
						"id": "localPath",
						"title": "Local Path",
						"type": "text",
						"value": ".",
						"caption": "The base filesystem path to find files under."
					},
					{
						"id": "filespec",
						"title": "Filename Pattern",
						"type": "text",
						"value": ".+",
						"caption": "Optionally filter the local files using a regular expression, applied to the filenames."
					},
					{
						"id": "remotePath",
						"title": "Remote Path",
						"type": "text",
						"value": "",
						"caption": "The base S3 path to store files under.",
						"required": true
					}
				]
			},
			{
				"id": "listFiles",
				"title": "List Files",
				"description": "Generate a file listing of an S3 prefix",
				"fields": [
					{
						"id": "remotePath",
						"title": "Remote Path",
						"type": "text",
						"value": "",
						"caption": "The base S3 path to look for files under.",
						"required": true
					}
				]
			}
		]
	}
}

Here the toolset menu would show two tools: "Upload Files" and "List Files". When "Upload Files" was selected in the menu, three new sub-parameters would appear in a box under the menu: "Local Path", "Filename Pattern" and "Remote Path". If the user selected a different tool, e.g. "List Files", then the sub-parameters would change, and a different set would be shown.

Tool fields use the same internal format as plugin parameters, but only checkbox, code, json, hidden, select, text and textarea field types are allowed inside a toolset.

For text fields inside a toolset, you can use all the same text variants described above. In other words, a tool field with "type": "text" may also include "variant": "date", "variant": "number", "variant": "url" and so on. These are still normal text fields in the toolset schema, but they are rendered in the browser as native input type=VARIANT controls.

Here is another example showing all the available field types in a single tool:

{
	"type": "toolset",
	"id": "tool",
	"title": "Tool Select",
	"caption": "",
	"data": {
		"tools": [
			{
				"id": "sample",
				"title": "Sample Tool",
				"fields": [
					{
						"id": "txt",
						"title": "Text Field",
						"type": "text",
						"value": ""
					},
					{
						"id": "txta",
						"title": "Text Area",
						"type": "textarea",
						"value": ""
					},
					{
						"id": "cbox",
						"title": "Checkbox",
						"type": "checkbox",
						"value": false
					},
					{
						"id": "sel",
						"title": "Select",
						"type": "select",
						"value": "Frog,Toad,Cat,Dog"
					},
					{
						"id": "cod",
						"title": "Code Editor",
						"type": "code",
						"value": "#!/usr/bin/something"
					},
					{
						"id": "jos",
						"title": "JSON Editor",
						"type": "json",
						"value": { "foo": "bar" }
					},
					{
						"id": "hid",
						"title": "Hidden",
						"type": "hidden",
						"value": "boo"
					}
				]
			}
		]
	}
}

Note that when all the parameter values are collected from the user, they are "flattened" into a single-level object, and they share the namespace with all the other plugin parameters. As such, field IDs must be unique, and not collide with any other plugin parameters defined outside the toolset. The same field IDs can be used across tools, however, as only one tool will be selected at a time.

Group

Use the group control type to automatically group all controls below into a fieldset (a visual box), with a custom label and an optional Markdown-formatted caption. The group will encompass all controls below it, until one of the following is encountered:

  • Another named group
  • A toolset parameter
  • The end of the parameter list

Groups are visual separators only, and do not change any functionality, parameter names, etc.

Example group:

{
	"id": "group_resize",
	"title": "Resize",
	"type": "group",
	"caption": "Optionally resize each input image before any other operations are applied."
}

Groups also need a locally unique id (alphanumeric).

Macro Expansion

All Plugin Parameter string values support inline macro expansion using the common {{ mustache }} syntax. Using this feature you can dynamically insert values into parameters from arbitrary data passed into the job from a previous job (connected workflow node or launched by action). Here is how it works. Imagine that a previous job completes, and outputs the following data:

{
	"xy": 1,
	"code": 0,
	"data": {
		"animal": "frog",
		"color": "green"
	}
}

This data object is then passed into the next job's input (either by workflow or run event action). You could access the data directly in your Plugin by parsing the JSON from STDIN and looking in input.data. However, the idea with macro expansion is that user can reroute data values into Plugin parameters. Let's say your Plugin has a text field parameter, and the user populated it in the event configuration like this:

My favorite animal is {{ data.animal }}, and my favorite color is {{ data.color }}.

When the job runs, those {{ mustache }} placeholders are automatically expanded using the Job object as the context. In addition, the Job.input sub-object is "flattened" into the outer context for convenience (just so you can skip the input prefix in the macros). This allows you to access all the output data from the previous job in the current job, and copy it into Plugin parameters.

The mustache macros can do more than just data lookups. They can also evaluate simple JavaScript-style expressions as well. For more on this, see xyOps Expression Syntax.

Built-in Plugins

The following Event Plugins are built into xyOps and come preinstalled.

Shell Plugin

xyOps ships with a built-in "Shell Plugin", which you can use to execute arbitrary shell scripts. Simply select the Shell Plugin when creating an event or workflow, and enter your script. This is an easy way to get up and running quickly, because you don't have to worry about reading or writing JSON.

Here are the parameters it accepts:

Param NameParam IDTypeDescription
Script SourcescriptCodeEnter the shell script source to run. This parameter is Administrator Locked by default, so standard users and non-admin API Keys cannot change it unless an administrator unlocks the parameter or grants admin privilege.
Add Date/Time Stamps to LogannotateCheckboxPrefix non-JSON stdout lines with date/time stamps in the job output, so each line is easier to trace during long-running jobs.
Data PassthroughpassCheckboxLegacy option that copies the job input data into the job output data, so downstream workflow nodes or run-event actions receive the same data. For new workflows, Workflow Data is usually the better option.

Important

The Shell Plugin can execute arbitrary code on your servers. Keep the script parameter administrator locked unless you intentionally want non-admin users or API Keys to provide shell commands.

The Shell Plugin determines success or failure based on the exit code of your script. This defaults to 0 representing success. Meaning, if you want to trigger an error, exit with a non-zero status code, and make sure you print your error message to STDOUT or STDERR (both will be appended to your job's output capture). Example:

#!/bin/bash

# Perform tasks or die trying...
/usr/local/bin/my-task-1.bin || exit 1
/usr/local/bin/my-task-2.bin || exit 1
/usr/local/bin/my-task-3.bin || exit 1

You can still report intermediate progress with the Shell Plugin. It can accept JSON in the standard output format if enabled, but there is also a shorthand. You can echo a single number on its own line, from 0 to 100, with a % suffix, and that will be interpreted as the current progress. Example:

#!/bin/bash

# Perform some long-running task...
/usr/local/bin/my-task-1.bin || exit 1
echo "25%"

# And another...
/usr/local/bin/my-task-2.bin || exit 1
echo "50%"

# And another...
/usr/local/bin/my-task-3.bin || exit 1
echo "75%"

# And the final task...
/usr/local/bin/my-task-4.bin || exit 1

This would allow xyOps to show a graphical progress bar in the UI, and estimate the time remaining based on the elapsed time and current progress.

Tip

The Shell Plugin actually supports any interpreted scripting language, including Node.js, PHP, Perl, Python, and more. Basically, any language that supports a Shebang line will work in the Shell Plugin. Just change the #!/bin/sh to point to your interpreter of choice.

HTTP Request Plugin

xyOps ships with a built-in "HTTP Request" Plugin, which you can use to send simple GET, HEAD or POST requests to any URL, and log the response. You can specify custom HTTP request headers, and also supply regular expressions to match a successful response based on the content.

Here are the parameters it accepts:

Param NameParam IDTypeDescription
MethodmethodMenuSelect the HTTP request method, either GET, HEAD, POST, PUT or DELETE.
URLurlTextEnter your fully-qualified URL here, which must begin with either http:// or https://.
HeadersheadersText BoxOptionally include any custom request headers here, one per line.
POST DatadataText BoxIf you are sending a HTTP POST, enter the raw POST data here.
TimeouttimeoutNumberEnter the timeout in seconds, which is measured as the time to first byte in the response.
Idle Timeoutidle_timeoutNumberEnter the idle timeout in seconds, which is measured between data packets after the response begins.
Connect Timeoutconnect_timeoutNumberEnter the connection timeout in seconds, which is measured while opening the socket connection.
Success Matchsuccess_matchTextOptionally enter a regular expression here, which is matched against the response body. If specified, this must match to consider the job a success.
Error Matcherror_matchTextOptionally enter a regular expression here, which is matched against the response body. If this matches the response body, then the job is aborted with an error.
Follow RedirectsfollowCheckboxCheck this box to automatically follow HTTP redirect responses (up to 32 of them).
Download FiledownloadCheckboxCheck this box to attach the response body as a job output file instead of logging it as text.
SSL Cert Bypassssl_cert_bypassCheckboxCheck this box if you need to make HTTPS requests to servers with invalid SSL certificates (self-signed or other).

Request Chaining

The HTTP Request Plugin supports passing data between jobs. First, information about the HTTP response is passed into the job output data, so connected events can read and act on it. Specifically, the HTTP response code, all the HTTP response headers, and possibly even the content body itself (if formatted as JSON and smaller than 1 MB) are included. Example:

"data": {
	"statusCode": 200,
	"statusMessage": "OK",
	"headers": {
		"date": "Sat, 14 Jul 2018 20:14:01 GMT",
		"server": "Apache/2.4.28 (Unix) LibreSSL/2.2.7 PHP/5.6.30",
		"last-modified": "Sat, 14 Jul 2018 20:13:54 GMT",
		"etag": "\"2b-570fb3c47e480\"",
		"accept-ranges": "bytes",
		"content-length": "43",
		"connection": "close",
		"content-type": "application/json",
		"x-uuid": "7617a494-823f-4566-8f8b-f479c2a6e707"
	},
	"json": {
		"key1": "value1",
		"key2": 12345
	}
}

In this example an HTTP request was made that returned those specific response headers (the header names are converted to lower-case), and the body was also formatted as JSON, so the JSON data itself is parsed and included in a property named json. Downstream events that are linked to the HTTP Request job (either by workflow node or run event action) can read these properties and act on them.

Secondly, you can chain an HTTP Request into another HTTP Request, and use the chained data values from the previous response in the next request. To do this, you need to utilize a special {{ mustache }} template syntax in the second request, to lookup values in the data object from the first one. You can use these placeholders in the URL, Request Headers and POST Data text fields. Example:

  • URL: http://myserver.com/test.json?key={{ data.json.key1 }}
  • Headers: X-UUID: {{ data.headers['x-uuid'] }}

Here you can see we are using two placeholders, one in the URL and another in the HTTP request headers. These are looking up values from a previous HTTP Request event, and passing them into the next request. Specifically, we are using:

PlaceholderDescription
{{ data.json.key1 }}This placeholder is looking up the key value from the JSON data (body content) of the previous HTTP response. Using our example response shown above, this would resolve to value1.
{{ data.headers['x-uuid'] }}This placeholder is looking up the X-UUID response header from the previous HTTP response. Using our example response shown above, this would resolve to 7617a494-823f-4566-8f8b-f479c2a6e707.

So once the second request is sent off, after placeholder expansion the URL would actually resolve to:

http://myserver.com/test.json?key=value1

And the header would expand to:

X-UUID: 7617a494-823f-4566-8f8b-f479c2a6e707

You can chain as many requests together as you like, but note that each request can only see and act on chain data from the previous request (the one that directly chained to it).

Using Secrets in Requests

To use secrets in the HTTP Request Plugin, you need to specify them using a custom macro syntax: [secrets.KEY_NAME]. This square-bracket format works in the URL, the request headers, and the request body.

Note that the secrets will be printed on the job output screen.

Test Plugin

The Test Plugin exists mainly to test xyOps, but it can also be useful for testing pieces of workflows. It outputs sample data and optionally a sample file, which are passed to downstream events, if connected. It can also simulate various job outcomes (success, fail, etc.). It offers the following parameters:

Param NameParam IDTypeDescription
Test DurationdurationNumberThe number of seconds to run before reporting completion. Progress is always reported.
Simulate ResultactionMenuSelect which result to simulate (Success, Error, Warning, Critical, Crash).
Custom ValuecustomTextEnter an optional custom value to include in the test output data.
Burn Memory/CPUburnCheckboxIf checked the Plugin will use some memory and CPU (it will allocate 128-256MB of memory and use about 10% of a CPU core doing math in a loop).
Generate Network TrafficnetworkCheckboxIf checked the Plugin will make continuous network requests downloading large binary data blobs (from GitHub).
Upload Sample FileuploadCheckboxIf checked the Plugin will produce a sample file and attach it to the job output.

Fire Web Hook Plugin

xyOps ships with a built-in "Fire Web Hook" Plugin, which you can use to fire one of your configured Web Hooks as a standard job. This is useful when web hook delivery needs to be part of a workflow's actual job graph, rather than a follow-up Action that runs after another job has already completed.

The standard Web Hook action is still the best fit for notifications and side effects, where delivery failure should not change the original job's outcome. The Fire Web Hook Plugin is different: the web hook is the job. If the web hook succeeds, the job succeeds. If the web hook fails, the job fails, and you can branch, retry, abort the workflow, or run other follow-up logic based on that result.

Note

This Plugin is included with new installs starting in xyOps v1.0.69 and xySat v1.0.31. Existing installations upgraded from earlier versions may need to import the Fire Web Hook Plugin import file, because xyOps upgrades do not currently mutate existing system configuration.

Here are the parameters it accepts:

Param NameParam IDTypeDescription
Web Hookweb_hookSystem MenuSelect the configured xyOps web hook to fire. This field is required.
Custom TexttextText BoxOptionally enter custom text to append to the standard generated web hook message.

The selected web hook still uses the normal web hook definition, including its URL, method, headers, body, secrets, timeout, retries, redirect settings, TLS settings, and {{ ... }} template expressions. See Web Hooks for more details.

Note that the Custom Text parameter only matters if the selected web hook template uses the standard {{text}} value somewhere, for example in a JSON body field such as text, content, or message. If your web hook body does not reference {{text}}, then this extra text is not sent to the remote endpoint.

Docker Plugin

The Docker Plugin allows you to run custom scripts inside a Docker container. Similar to the Shell Plugin, you can specify any custom code to run, and in any language, as long as it supports a Shebang line.

You can enter any Docker image to use, including remote ones. By default, our own xyOps Shell Image is selected, which is based in Debian 12, and comes preinstalled with a variety of popular software, as well as our xyRun wrapper. xyRun will track system resources inside the container, as well as handle file upload/download for your jobs. This is optional, and you can use any Docker image you want, including your own custom ones.

This is built on top of docker run, so each job creates a new container, and can optionally delete it when the job is complete (which is the default behavior).

The Docker Plugin uses the following parameters:

Param NameParam IDTypeDescription
Image Nameimage_nameTextThe name of the Docker image to use, which can be local or remote.
Image Versionimage_verTextThe version of the image to use, or latest.
Container Namecont_nameTextThe name of the Docker container, which can use macros such as {{id}} to make it unique per job.
Max CPUscont_cpusNumberThe max number of CPU cores the container is allowed to use, or 0 for unlimited.
Max Memorycont_memTextThe max amount of memory to allow the container to use (default is unlimited).
Join Networkcont_netTextOptionally specify a Docker network name for the container to join.
Command Extrascont_extrasTextOptionally add any extra command-line arguments to pass to docker run (for e.g. volume mounts).
Launch Commandcont_cmdTextThe initial command to run as the container starts. It is recommended to use xyRun for this, so resources are monitored, and files are managed properly.
Run Moderun_modeMenuChoose whether you want the entire job JSON data to be sent to STDIN, or only the script source (advanced).
Script SourcescriptCodeThe code to run inside the container. You can use any language that supports a shebang line.
Init Process Managercont_initCheckboxRun an "init" inside the container that forwards signals and reaps processes.
Ephemeral Containercont_rmCheckboxAutomatically delete the container after the job completes (recommended).
Verbose LoggingverboseCheckboxEnable verbose debug logging (raw docker command, etc.)

Custom Images

Feel free to create your own custom Docker image for use in the Docker Plugin. You can either build one on top of ours, or build your own from scratch. Either way, we highly recommend you install xyRun inside your image as a command wrapper, so xyOps can track system resource usage, and manage files for your jobs.

If you use an image without xyRun, please note the following caveats:

  • Environment variables will not be set (i.e. JOB_ID, JOB_NOW, etc.).
  • Secrets will not be passed into the container.

To use a pre-existing Docker image such as ubuntu, you can set the launch command to something like sh, and then set the "Run Mode" to "Script Source". This will pipe in your script source directly to the STDIN of the launch process, e.g. sh, which will execute it inside the container.

Plugin Marketplace

xyOps has an integrated Plugin Marketplace, so you can expand the app's feature set by leveraging Plugins published both by PixlCore (the makers of xyOps), as well as the developer community. For more on this, please see the Marketplace Guide.