Skip to content
You're viewing the v2 docs. Looking for v1?Go to v1 docs
Docs
Testsigma
Popular questions
↑↓ to navigate↵ to selectesc to close
Book a demo

Build your own addon

Building an addon follows the same shape on both engines: create it in Testsigma, download the scaffold, write your code, validate it, zip it, upload it, publish it. What differs is the language and the tooling.

  1. Click the Addons icon in the left navigation bar and select Addons.

  2. Click + New Addon.

  3. Select the engine, either Classic or Modern (TypeScript).

  4. Enter a Name and a Description.

  5. Click Create & Proceed.

  6. Click the Download icon on the addon’s detail page to get the scaffold as a zip.

You need Core Java on OpenJDK 11, an IDE such as IntelliJ or Eclipse, Maven or Gradle, Selenium basics, Lombok annotations, and JUnit or TestNG configured as the test runner.

A Java Maven project with a pom.xml and sample templates for Web, Mobile Web, Android, and iOS Application.

Unzip it, open the folder in your IDE as a Java project with Maven as the build tool, and refactor the samples:

  • Action text: the step grammar test authors will read
  • Selenium or Java code: the logic the action performs
  • Elements and locators: what the action acts on
  • Test data: the values the test class passes in

Right-click the test class in your IDE and run it as a JUnit or TestNG test. Confirm the addon behaves as expected before uploading.

Save your changes and zip the project folder:

Terminal window
zip -r addonName.zip . -x "*"

The Addons page, with New Addon beside the Community, Installed Addons, and My Addons tabs

An action becomes a step with a grammar you define.

import { defineAddonTemplate } from "@testsigma/addon-sdk";
export const readText = defineAddonTemplate({
name: "myaddon.readText",
description: "Read the text content of a located element.",
actionText: "Read text from ${target}",
applicationType: ["WEB"],
elements: {
target: { description: "Element to read text from" },
},
outputs: {
value: { type: "runtimeVar", description: "The element's text content" },
},
async execute(input, ctx) {
const text = (await ctx.ui.browser.locate(input.elements.target).textContent()) ?? "";
return {
message: {
success: `Read ${text.length} character(s)`,
failure: `Could not read text`,
},
outputs: { value: text },
};
},
});

Every input key has to appear in actionText, and every placeholder has to match an input key. An empty applicationType is a compile error.

input.testData holds values typed by your Zod schema and already validated, input.elements holds one opaque ElementRef per declared element to pass to locate(), and input.environment holds values read from the test environment. A .describe() text on a Zod field becomes the inline help a test author reads, so describe every field.

Typing a testData field as z.enum([...]) renders a fixed-option dropdown instead of a free-text field:

testData: z.object({
priority: z.enum(["Low", "Medium", "High"]).describe("Ticket priority"),
}),

The build records the options as testDataAllowedValues in the manifest, and the same enum validates the submitted value at run time.

When a test author supplies an uploaded file as test data, your addon receives a local file path staged into ctx.tmpDir. The platform downloads the file before execute() runs, and you read it with node:fs. No fs permission is needed, because ctx.tmpDir is always allowed.

Both message fields are mandatory, so both outcomes get addressed up front. Make them specific: “Read 42 characters from Order total” tells a test author more than “Success”. A thrown Error fails the step and surfaces its message verbatim, and returning conditionMet: false fails it with message.failure, which is often clearer than throwing.

The applicationType list decides which adapter ctx.ui exposes.

applicationTypectx.ui
["WEB"]ctx.ui.browser
["ANDROID"] or ["IOS"]ctx.ui.mobile
["WEB", "ANDROID"]A union, narrowed with ctx.ui.kind

For a cross-platform addon, branch on ctx.ui.kind rather than shipping one bundle per platform.

Five habits account for most of the difference between an addon that is easy to debug and one that is not.

  • Write both message.success and message.failure, and make them specific
  • Log liberally with ctx.logger. Logs are captured with the step result and are your main debugging tool in a cloud run. Never log secrets
  • Honor ctx.signal in loops and long waits, and use ctx.tmpDir for files
  • Describe every field, because those strings become the inline help a test author reads
  • Validate with Zod constraints rather than checks inside execute()

Addon code runs in a sandbox where everything is denied by default. You opt into each capability with an allowlist on the definition, and the build mirrors it into the manifest.

permissions: {
fs: ["/data/exports/**"], // filesystem globs
net: ["api.example.com"], // outbound hostnames
env: ["MY_API_TOKEN"], // readable process.env keys
spawn: true, // ctx.spawn, local agent only
ai: true, // ctx.ai
ocr: true, // ctx.ocr
}
PermissionDefaultUnlocksOn violation
fstmpDir onlyReading and writing the listed globsThe filesystem call throws
netNo networkfetch to the listed hostnamesThe request is rejected
envNo accessReading the listed process.env keysThe key reads as undefined
spawnfalsectx.spawn.run()On cloud, every call rejects
aifalsectx.aiThe field is absent
ocrfalsectx.ocrThe field is absent

fs, net, and env apply to actions, test data functions, and hooks alike. spawn, ai, and ocr are honored for action addons only, so hook and test data function contexts never expose them. ctx.tmpDir is always readable and writable without an fs entry.

Subprocess execution is host-brokered, running in the agent process through execFile with a command and an argument vector and no shell, which is why raw child_process imports stay blocked.

Some things no permission unlocks. Dynamic code evaluation is blocked, including eval and the raw Function constructor. So are raw child_process, worker_threads, and the raw node:http, node:https, and node:net modules, and a top-level import of one stops the bundle loading at all. Make outbound calls with the global fetch behind permissions.net, and run local commands with ctx.spawn behind permissions.spawn.

Reviewers and test authors see your declared permissions, so a tight allowlist makes an addon easier to trust and approve.

const { stdout, exitCode } = await ctx.spawn.run("node", ["-e", "console.log('hi')"]);
  1. Open the addon’s detail page.

  2. Click Upload Code on Classic, or Upload Zip File on Modern.

  3. Select the zip and confirm.

Testsigma reads the package and registers the actions, test data functions, and hooks it declares. The dropdown also offers Edit Description, Manage Tags, and Deprecate.

  • manifest.json sits at the root of the zip
  • The bundle sits at the path the manifest’s entry field points to
  • Every capability is a named export, with no default export and no arrays
  • Names are unique within the bundle
  • Maximum upload size is 50 MB

An upload is rejected, or the version marked Failed, when the manifest is invalid. The usual causes are an addon entry with an empty applicationType, and an entry path that does not point at the bundle inside the zip.

A newly uploaded Modern addon stays in Draft, which runs on local and hybrid agents so you can iterate without publishing. Cloud executions need a published addon.

StatusLocal and hybrid agentsCloud executions
DraftRunsNot shipped
PublishedRunsRuns

Select Publish from the dropdown on the addon’s detail page. On Classic, choose Public to release it to the whole Testsigma community or Private to keep it inside your organization, then click Publish. An automatic security check runs, and a failure is emailed to you.

Modify and re-validate the code, then go to Addons, open the My Addons tab, select the addon, and upload the new package.

Open a test case, click Add New Step, and search for the action by keyword. An addon step carries an Addon icon before it. Select it, fill in the test data and elements, and click Create Step.

Add the OCR dependencies to pom.xml, then implement the OCR interface and update 3 methods in the Java module:

  • extractTextFromPage() extracts text from a whole page
  • extractTextFromImage(OCRImage image) extracts text from an OCRImage object
  • extractTextFromElement(Element element) extracts text from an Element object

All three return a list of OCRTextPoint objects, each carrying the text’s location within the source.

Query a database without building an addon

Section titled “Query a database without building an addon”

MySQL, MongoDB, PostgreSQL, and Oracle all have published addons, so a database query needs no addon of your own. Go to Addons > Add-ons, find the addon under New & Updated Addons, and click Install. Its NLPs then appear in Test Steps.

The mysql_queries addon runs MySQL over a JDBC URL of the form jdbc:mysql://<hostname>:<port>/<database>?user=<user>&password=<password>. Its NLPs cover:

  • Executing a query, a create-procedure statement, a stored procedure call, an update, or a .sql script file
  • Storing a select result into a variable
  • Verifying a select result, or the affected-row count of a query
  • Comparing two queries on one connection, or the same query across two connections

The MongoDB_NLPs addon takes these parameters:

ParameterWhat it holdsExample
DBnameThe MongoDB database holding the collectionse-commerce
CollectionNameA group of MongoDB documentsusers
MongoDB_ConnectionThe connection to a server or clusterMongoClient
MongoDB_ConnectionURLThe server or cluster address, with connection optionsmongodb://localhost:27017/blog

The OracleDB_Queries addon runs a query and verifies the affected row count, through the Execute OracleDB Query on the Connection DB_Connection_URL and verify affected rows count is Row-Count NLP. Its DB_Connection_URL takes this form:

jdbc:oracle:thin:@//<host>:<port>/<service_name>?user=<username>&password=<password>

The PostgreSQL_Queries addon takes these:

ParameterWhat it holdsExample
QueryA query to executeSELECT * FROM table_name;
Row_CountThe number of rows in a result setSELECT COUNT(*) FROM table_name;
DB_ConnectionURLA connection URLpostgresql://user:password@localhost:5432/database_name
PG_DBConnectionURLA connection URL, host and port formpostgresql://username:password@host:port/database_name
Select-QueryA SELECT query returning a result setSELECT column_name FROM table_name WHERE condition;
Variable-NameThe variable a value is assigned tovar_name
Expected-ValueThe value a result is compared againstThe value you expect the query to return
SymptomCause and fix
“Input X is declared but never referenced in actionText”Every testData, elements, and environment key has to appear in actionText as ${X}. Add the placeholder, or remove the unused field
“actionText references ${X} but no input field X is declared”A placeholder has no matching key, usually a typo. The CLI suggests the closest match. Output keys must not appear in actionText
Upload rejectedThe zip is over 50 MB, manifest.json is missing from the zip root, entry does not point at the bundle inside the zip, or applicationType is empty
The bundle fails to load after uploadA top-level import of an always-blocked module, duplicate export names, or non-ESM output

Check these in order:

  1. The application runs on the Classic engine, which offers Java addons only.
  2. The manifest entry is missing, or its applicationType excludes the workspace platform.
  3. The addon is still in Draft and the run is a cloud execution.

A “not in permissions.net” error means the hostname, path, or environment key is absent from your allowlist. Add it to permissions in the definition, rebuild, and re-upload. The manifest regenerates itself.

Hooks and test data functions enforce the same sandbox, so declare permissions on defineHook() and defineTestDataFunction() too.

SymptomCause and fix
An output never lands in a runtime variableThe key returned in outputs does not match the declaration, or the test author did not map a variable name
A mobile locator fails with a strategy errorandroidUiAutomator is Android only, and iosPredicate and iosClassChain are iOS only. Branch on the platform, or use accessibilityId, id, xpath, or className
A hook never firesOnly AFTER hooks run today. Confirm the hook is attached to the test plan and the manifest entry has "hookType": "AFTER"
A Zod validation error on executeThe test author’s input does not satisfy your schema. Add clearer field descriptions and defaults, so a test author supplies valid input without guessing
ctx.spawn.run() rejects with “local agent only”Subprocess execution is unavailable on cloud executions. Run on a local or hybrid agent

Was this page helpful?