• Documentation
  • Tutorials
  • Blogs
  • Product

What's on this Page

  • Keyword API releases and versions
  • Keyword declaration
    • Keyword annotation
  • Keyword inputs
    • Reading the inputs in code
    • Passing inputs from code
    • Passing inputs from the Step plan
  • Keyword properties
  • Keyword access to Automation Package Archive
  • Keyword outputs
    • Custom fields
    • Attachment
    • Measurements
    • Metrics
    • Error handling
  • Keyword execution hooks
    • onError hook
    • beforeKeyword and afterKeyword hooks
  • Session
    • Storing and accessing objects
  • Keyword Proxy: plans as Code
    • How it works
  • Live Reporting
    • Sample usage
    • Live Metrics
  • Step
  • Developer guide
  • Keyword API
Categories: DEVELOPER GUIDE API
This article references one of our previous releases, click here to go to our latest version instead.

Keyword API

For a better understanding of the key concepts and best practices of keywords development, make sure to read first the Keyword development page.

The versatility of Step and its keyword API allows developers to implement their own custom Step’s Keywords whether they use 3rd party automation framework or their own libraries.

The Keyword API defines a clear interface to:

  • define and configure Keywords
  • access Keyword inputs, session objects, and report data (keyword outputs, performance measurements, attachments)
  • implement hooks related to keyword executions (interceptors, error management)

The Keyword API is natively available for Java, .NET and JavaScript/TypeScript.

Keyword API releases and versions

The Keyword API is maintained and versioned independently of the Step platform, you will find the release history of the Keyword API as well as the compatibility with Step versions on the dedicated release notes page.

Keyword declaration

Declaring a keyword with the API is quite simple but varies a bit depending on the target language.

    Declaring a keyword in Java requires to extend the AbstractKeyword superclass and annotate the keyword’s function with the Keyword annotation . More details about this class can be found in the javadoc.

    public class MyKeywords extends AbstractKeyword{
      @Keyword
      public void myKeyword() {
        //my automation
      }
    }

    Declaring a keyword in .Net requires to extend the class with the AbstractScript superclass. Each method defining a keyword is then annotated with the Keyword annotation, it must have no arguments and return type void.

    namespace STEP {
      public class Keywords : StepApi.AbstractScript {
        [Keyword(name = "My Keyword")]
        public void MyKeyword() {
          //your implementation
        }
      }
    }

    Keywords are exported async functions from CommonJS .js files placed in the project’s keywords/ directory (configurable via step.keywords in package.json). The export name is the keyword name as registered in Step.

    exports.MyKeyword = async (input, output, session, properties) => {
      const result = await doSomething(input['param'])
      output.add('result', result)
    }

    Each keyword receives four arguments:

    • input — the input parameters sent by the Step plan, as a plain object.
    • output — an OutputBuilder used to build the return value. Use output.add(key, value) to set output fields, output.setError(msg) or output.fail(msg) for errors, and output.attach(…) to add file attachments. Calling output.send() is supported for backwards compatibility but no longer required.
    • session — a Map scoped to the token’s lifetime, allowing state to be shared between consecutive keyword calls within the same session (e.g. a browser or database connection).
    • properties — a flat key/value map of agent and token properties configured in Step.

    Throwing an unhandled error marks the keyword as failed.

    Three optional module-level hooks can be exported alongside keywords (in the same module):

    • beforeKeyword(functionName) — called before each keyword execution.
    • afterKeyword(functionName) — called after each keyword execution, even on failure.
    • onError(exception, input, output, session, properties) — called when a keyword throws an error; return true to propagate the error, false to suppress it.

    Keyword annotation

    The Keyword annotation as following optional attributes which can be used:

    • name: the name of this keyword. If not specified the method name is used as keyword name
    • description: a text describing the keyword usage. It will be displayed in the gui
    • properties: the list of properties required by this keyword
    • optionalProperties: the list of optional properties which might be used by this keyword
    • schema: the JSON schema of the input object
    • timeout: the keyword timeout in milliseconds
    • routing: the keyword routing rules definition which can either be:
      • the reserved word "controller" to execute the keyword directly on the controller rather than on agents
        Example: @Keyword(routing = {Keyword.ROUTING_EXECUTE_ON_CONTROLLER})
      • the agent token selection criteria provided as a list of key-value pairs
        Example: @Keyword(routing = {"OS", "Windows","type","playwright"})
    • planReference: the path to the file containing a plain text plan; used to define a composite keyword

    More details on the usage and impact of these attributes are described here.

    Keyword inputs

    A Keyword’s input is a JsonObject with opened types. Thus the developer can choose the type of their arguments but must ensure that they are read accordingly to that type, otherwise the Json reader will throw an error.
    The inputs are passed to the keyword by the caller as described in the following sections.

    Reading the inputs in code

    Inside the Keyword, you can then retrieve your inputs like this:

      String homeUrl = input.getString("url");
      int elementIndex = input.getInt("index",1);
      String homeUrl = (string)input["url"];
      var homeUrl = input['url'];

      Reading inputs from method arguments

      For Java, there is an easier alternative to define and access inputs: directly as method arguments. In such case, the schema specifying the keyword’s inputs is also generated automatically.

      In this case, the @Input annotation is used for each method parameters. This annotation supports following properties:

      • name: the name of input (must be defined for method parameter)
      • defaultValue - the default value to be used if the input is not defined when calling the keyword (optional)
      • required - if true, the value of this input must always be provided (false by default)

      You can annotate the following method arguments with @Input:

      • String
      • Integer (or int)
      • Long (or long)
      • Double (or double)
      • BigDecimal
      • BigInteger
      • Array or Collection with any of supported types
      • Object:
        • Maps of strings to any supported types are used
        • Pojo composed of accessible fields are also supported (setAccessible(true) is used when allowed by your Agent’s JVM)

      Some examples explaining how to use the @Input annotation:

        public class KeywordTestClass extends AbstractKeyword {
            @Keyword
            public void MyKeywordWithInputAnnotation(
                    @Input(name = "myRequiredInputNumber", required = true) int myRequiredInputNumber,
                    @Input(name = "myInputNumber", defaultValue = "3") int myInputNumber,
                    @Input(name="myInputBoolean", defaultValue = "true") boolean myInputBoolean,
                    @Input(name = "myInputString", defaultValue = "default string value") String myInputString,
                    @Input(name = "stringListInput", defaultValue = "string1;string2") List<String> stringList,
                    @Input(name = "mapStringInput", defaultValue = "{\"key1\":\"valueStr1\",\"key2\":\"valueStr2\"}") Map<String, String> stringMap,
                    @Input(name = "longListInput", defaultValue = "1223456;55555") List<Long> longList,
                    @Input(name = "mapLongInput", defaultValue = "{\"key\":3}") Map<String, Long> longMap,
                    @Input(name = "booleanListInput", defaultValue = "true;false;true") List<Boolean> booleanList,
                    @Input(name = "mapBooleanInput", defaultValue = "{\"key\":true}") Map<String, Boolean> booleangMap) {
            }
        }

        Passing inputs from code

        When calling the keyword from your code (for instance in JUnit), you can pass the input as parameter:

          ExecutionContext ctx = KeywordRunner.getExecutionContext(properties, this.getClass());
          // { "url" : "http://step.dev", "index" : 3 }
          Output<JsonObject> output = ctx.run("MyKeyword", "{\"url\":\"http://www.exense.ch\", \"index\" : 3 }");

          Use the runner utility to execute keywords locally before deploying them to Step:

          const runner = require('step-node-agent/api/runner/runner')({
            /* optional: properties made available to all keywords, equivalent to agent properties in a real deployment */
            myProperty: 'value'
          })
          
          try {
            const output = await runner.run('MyKeyword', { param: 'value' })
            console.log(output.payload)
          } finally {
            runner.close() // releases the session
          }

          The properties passed to the runner constructor are merged into every keyword call. Call runner.close() when finished to dispose all session resources.

          Passing inputs from the Step plan

          When calling the Keyword from a plan, you can pass the input as parameter as shown in below screenshots. More details can be found in the dedicated documentation for Plans: Screenshot showing example keyword inputs

          If the Keyword has an attached schema required and optional inputs can be defined

          Keyword properties

          Keywords can also access a map of properties which contains all variables in the scope of an execution. This includes variables defined:

          • In the properties of the agent executing the keyword
          • In the plan calling the keyword using “Set”
          • In the parameters defined in the controller
          • In the Keyword configuration such as:
          • $keywordName: the name of the keyword
          • $keywordTimeout: the defined execution timeout of this keyword
          Note that if the same variable is defined at multiple places the values in agent properties override the one in plan which overrides the one in parameter.

          You can retrieve the properties in your keywords from the properties map

            properties.get("myVar");
            properties["myVar"]
            properties['myVar']

            Keyword access to Automation Package Archive

            Java Keywords deployed via Automation Packages have the possibility to retrieve and extract the archive at runtime. This enables you to bundle any resources that your keyword may require and access it in your code as follows:

              if (isInAutomationPackage()) {
                  File extractedArchiveFolder = retrieveAndExtractAutomationPackage();
                  ...
              }
              //Not supported
              //Not supported
              

              Keyword outputs

              Each keyword can define the content of its output by using the inherited “output” object from its parent class. This output builder object is mainly used to:

              • add any key/value pair information for example:
                • for functional checks in plan
                • required for workflow execution
              • set keyword execution status
              • add attachments (pdf, screenshots, exception stack trace as text file…)
              • define custom response time measurements for analytics

              The method of the OutputBuilder class can be found in the javadoc. The same methods are available for .net. For node.js, you may refer to the source code on github.

              Custom fields

              Below are example on how to add key/value pair information to your keyword’s output object.

                You can refer to the javadoc for the full list of available method. Below is an example for adding a field with a String value:

                output.add("field_name", "field_value");
                output.add("field_name", "field_value");
                // String
                output.add('field_name', 'string value')
                // Number
                output.add('field_name', 42)
                // Boolean
                output.add('field_name', true)

                Java output alternative

                In Java, instead of manually reporting custom outputs as shown above, your Keyword method can directly return a POJO, which will automatically be mapped to the corresponding output object.

                Supported returned object types:

                • Maps of strings to any of the types which are supported and listed for the input annotation
                • Pojo composed of accessible fields (setAccessible(true) is used when allowed by your Agent’s JVM)

                Note: You can either report outputs using the output object (output.add) or return a POJO but not both at the time.

                Attachment

                This section refers to attachments that are generated as part of the keyword output. For streaming data in a realtime fashion, see the section on Live Reporting below.

                Below is an example for attaching files to the Keyword’s output:

                  byte[] bytes = Files.readAllBytes(file.toPath());
                  Attachment attachment = AttachmentHelper.generateAttachmentFromByteArray(bytes, outputName+".log");
                  output.addAttachment(attachment);    
                  Attachment a = AttachmentBuilder.generateAttachmentForException(exception);
                  output.addAttachment(a);
                  // Create an attachment based on a string
                  output.attach({ name: 'test.log', hexContent: Buffer.from('This is a test log').toString('base64') })
                  // Create an attachment based on an array of bytes
                  output.attach({ name: 'screenshot.png', hexContent: Buffer.from(data).toString('base64') })
                  // Attach a file read from disk, with a specific mimeType
                  output.attach({ name: 'trace.zip', hexContent: fs.readFileSync(tracePath).toString('base64'), mimeType: 'application/vnd.step.playwright-trace+zip' })
                  Despite the name, the hexContent field must be base64-encoded (not hex). Use Buffer.from(data).toString('base64') to produce the correct encoding.

                  Attachment MIME type

                  It is possible to specific the MIME type of the attachment for both legacy and streaming attachments. This is particularly useful to benefit for custom viewer in the UI such as the Playwright trace viewer.

                  byte[] bytes = Files.readAllBytes(tempFile);
                  output.addAttachment(AttachmentHelper.generateAttachmentFromByteArray(bytes, "trace.zip", CommonMimeTypes.APPLICATION_PLAYWRIGHT_TRACE));

                  Attachment size

                  The Step controller limit the size of the attachment by default to 50 Mbytes, this can be changed with following step.properties

                  grid.client.max.string.length.bytes=50000000
                  

                  Attachment quota per execution

                  The Step controller limits the number of attachments per execution by default to 100. This quota can be modified by setting following variable in your plans

                  tec.quota.attachments
                  

                  Measurements

                  This section refers to measurements that are generated as part of the keyword output. For streaming data in a realtime fashion, see the section on Live Reporting below.

                  Measurements are automatically created by the controller when executing a keyword. This represents the keyword execution time from the controller view (but excluding any delegation time to the agents). You can create custom response time measurements inside your keyword to have finer granularity and/or to enrich the measurements with any key/value pair for analytics purposes.

                  Measurements are opened in a stack mode: when calling stopMeasure, the most recently opened measurement is closed.

                  By default, measurements inherit the final status of the keyword that produced them (PASSED, FAILED, or TECHNICAL_ERROR). You can also set an explicit status on each individual measurement using Measure.Status.

                  Refer to the OutputBuilder class’s javadoc in your preferred IDE for an exhaustive documentation of the API.

                    Opening and closing measurements

                    output.startMeasure("db-query");
                    // ... timed code ...
                    output.stopMeasure();                                          // status inherits from keyword outcome
                    
                    output.startMeasure("payment-call");
                    // ... timed code ...
                    output.stopMeasure(Measure.Status.FAILED);                     // explicit status
                    
                    output.startMeasure("notification");
                    // ... timed code ...
                    output.stopMeasure(Map.of("channel", "email"));                // custom data, no explicit status
                    
                    output.startMeasure("login");
                    // ... timed code ...
                    output.stopMeasure(Measure.Status.PASSED, Map.of("user", "alice")); // status + data
                    

                    Measure.Status values: PASSED, FAILED, TECHNICAL_ERROR.

                    Recording a pre-known duration

                    output.addMeasure("external-call", 350L);
                    output.addMeasure("external-call", 350L, startTimestamp);
                    output.addMeasure("external-call", 350L, Map.of("host", "api.example.com"));
                    output.addMeasure("external-call", 350L, startTimestamp, Map.of("host", "api.example.com"));
                    

                    Nested measurements example

                    output.startMeasure("CustomMeasureInKeyword");
                    // Do some automation work
                    output.startMeasure("CustomMeasureInKeyword_Inner");
                    // Do some further work to be measured with finer granularity
                    output.stopMeasure(); // closes "CustomMeasureInKeyword_Inner"
                    // Add custom data and close "CustomMeasureInKeyword"
                    output.stopMeasure(Map.of("username", "Smith"));
                    
                      output.startMeasure("Navigate");
                      //do something
                      output.stopMeasure();
                    output.startMeasure('Custom measure in Keyword')
                    // Do some automation work
                    output.stopMeasure()
                    
                    output.startMeasure('Second custom measure in Keyword')
                    // Stop the measure and assign it a status
                    output.stopMeasure({ status: 'FAILED' })
                    
                    // Add pre-timed measures
                    output.addMeasure('Pre-timed measure', 50)
                    output.addMeasure('Pre-timed measure 2', 150, { status: 'TECHNICAL_ERROR', begin: Date.now() - 150, data: { info: 'test' } })

                    Metrics

                    This section refers to metrics that are generated as part of the keyword output. For streaming metrics in real time during keyword execution, see Live Metrics in the Live Reporting section below.

                    In addition to measurements (which capture the timing of individual steps), keywords can emit metrics: named instruments that aggregate observations during a keyword invocation. Three instrument types are available:

                    Type Use case Method
                    Counter Monotonically increasing totals (request count, error count) increment(n)
                    Gauge Values that can rise and fall (queue depth, active connections) observe(value)
                    Histogram Distribution of observed values (response times, payload sizes) observe(value)

                    Metrics are registered via factory methods on output. Samples are collected and added to the output as follows:

                    • A sample captures all observations or increments accumulated since the previous sample
                    • Samples are collected at a fixed interval of 5 seconds
                    • A final sample is flushed when the keyword completes

                    The sampling interval is not currently configurable.

                    Labels

                    Every metric can carry an optional set of labels — a flat map of string key/value pairs attached when the metric is created. Labels are stored on every snapshot that the metric produces and flow through Step’s internal analytics pipeline, where they can be used to filter and group metrics in dashboards (e.g. chart response_time_ms broken down by endpoint or environment).

                    Labels are set at metric creation and cannot be changed afterwards — all observations on a metric carry the same label set.

                    The following label names are reserved by Step’s internal time-series pipeline and will be silently overwritten if used: name, metricType, instrumentType, eId, planId, plan, canonicalPlanName, agentUrl, taskId, schedule, execution, origin. Avoid these names in your label maps.
                      // Counter
                      CounterMetric requests = output.newCounter("requests");
                      requests.increment();    // +1
                      requests.increment(5);   // +5
                      
                      // Counter with labels
                      CounterMetric labeled = output.newCounter("requests", Map.of("service", "checkout"));
                      labeled.increment(3);
                      
                      // Gauge
                      GaugeMetric queueDepth = output.newGauge("queue_depth");
                      queueDepth.observe(12);
                      queueDepth.observe(7);
                      
                      // Histogram
                      HistogramMetric responseTimes = output.newHistogram("response_time_ms");
                      responseTimes.observe(120);
                      responseTimes.observe(340);
                      
                      // Histogram with labels
                      HistogramMetric labeled = output.newHistogram("response_time_ms", Map.of("endpoint", "/login"));
                      labeled.observe(200);
                      

                      If you build a metric object separately (e.g. in a helper class), register it with addMetric:

                      CounterMetric errors = new CounterMetric("errors");
                      errors.increment(2);
                      output.addMetric(errors);
                      

                      Error handling

                      Step makes a difference between errors which are internal (status “TECHNICAL_ERROR”) and business errors (status “FAILED”). Any unhandled exception raised during the execution of a keyword will end up in the status “TECHNICAL_ERROR”. In order to produce clean reports and leverage this distinction, we highly recommend catching any exception being thrown from within your keywords and handle the situation adequately.

                      Errors can be handled directly withing the keyword’s function or by implementing the on error.

                      Technical Errors

                      In case your still want your keyword to end up in a technical error state, you may use one of the setError methods available in all supported languages. Likewise, any exception which is thrown from the Keyword will result in a technical error. Remember that this is not the preferred approach as internal error should be reserved for Step technical errors.

                      Business errors

                      To manage properly any errors related to the system or application which is being automated, you may use the setBusinessError method. This will automatically set the status of the keyword execution to “FAILED”.

                      If the status as to be determined outside of the keyword, add any meaningful values to the output and perform the check in the caller (i.e. in a Step plan).

                      In case of exception, you can attach the detailed information to the output object such as the exception message. You can even attach binary content or the exception stack trace to the object by doing so:

                        output.add("ExceptionMessage",e.getMessage());
                        output.addAttachment(AttachmentHelper.generateAttachmentForException(e));
                        Attachment a = AttachmentBuilder.generateAttachmentForException(exception);
                        output.addAttachment(a);

                        The simplest way to attach an exception’s stack trace is to pass the Error object directly to setError() — it attaches the stack automatically:

                        try {
                          // automation work
                        } catch (e) {
                          output.setError(e)          // sets error message AND attaches e.stack as exception.log
                          // or with a custom message:
                          output.setError('Custom message', e)   // message shown in Step, stack attached as exception.log
                        }

                        If you need to attach the trace manually:

                        output.attach({
                          name: 'exception.log',
                          description: 'exception stacktrace from keyword',
                          hexContent: Buffer.from(e.stack).toString('base64')  // base64-encoded despite the field name
                        })

                        Keyword execution hooks

                        onError hook

                        For each language, overriding the onError hook offers a last chance to manage unhandled exceptions. Each time your keyword function throws an exception, this function will be called with the exception as argument. The return value of this function determine if the exception should be re-thrown (return true) or ignored (return false):

                        • If re-thrown, the status of the execution will be reported as a “TECHNICAL_ERROR” and an attachment of the exception trace will be added
                        • If ignored, the status will be set as “PASSED” and no error will be reported

                        Overriding this function is done as follow:

                          @Override
                          public boolean onError(Exception e) {
                            /* do here cleanup or exception reporting */
                            return false;
                          }
                          public override bool onError(Exception e)
                          {
                            /* do here cleanup or exception reporting */
                            return false;
                          }
                          exports.onError = async (exception, input, output, session, properties) => {
                            /* do here cleanup or exception reporting */
                            // Return false in order not to rethrow the exception
                            return false;
                          }

                          beforeKeyword and afterKeyword hooks

                          For each language, overriding the beforeKeyword or afterKeyword hooks allow you to execute management code before or after a keyword is called. Note that the afterKeyword function will always be called, even if an exception occurs during the keyword execution or during the beforeKeyword call. The onError call is called before the afterKeyword in case of exception.

                          Overriding these functions is done as follow:

                            @Override
                            public void beforeKeyword(String keywordName, Keyword annotation) {
                              /* do here some management */
                              System.out.println("Calling "+keywordName);
                            }
                            @Override
                            public void afterKeyword(String keywordName, Keyword annotation) {
                              /* do here some management */
                              takeScreenshot();
                            }
                            public override void BeforeKeyword(string KeywordName, Keyword Annotation)
                            {
                              /* do here some management */
                              Console.WriteLine("Calling "+keywordName);
                            }
                            
                            public override void AfterKeyword(string KeywordName, Keyword Annotation)
                            {
                              /* do here some management */
                              TakeScreenshot();
                            }

                            exports.beforeKeyword = async (functionName) => { // Do something before each execution of the keywords defined in the same module }

                            exports.afterKeyword = async (functionName) => { // Do something after each execution of the keywords defined in the same module }

                            Session

                            Step provides the ability to store data in a session object. This object usually only makes sense when a Session control is used inside the test plan. The session object becomes very useful when passing information or technical objects between keywords (see for example the way we use Selenium’s driver in the next section), especially if the data is difficult or impossible to serialize and de-serialize via the input/output mechanism.

                            Storing and accessing objects

                            Storing by name

                            You can set any kind of data (primitive types or collections) in the session object as follow :

                              @Keyword(name="PutToSession")
                              public void putToSession(){
                                session.put("string_key", "Here is my value");
                                session.put("int_key", 3);
                              }
                              session.put("driver", new Wrapper(driver));
                              exports.MyFirstKeyword = async (input, output, session) => {
                                session.set('sharedKey', 'My value')
                              }

                              You can then access your data from another keyword the same way, but you’ll have to cast the data manually back to its original type :

                                @Keyword(name="GetFromSession")
                                public void getFromSession() {
                                  output.add("my_string_value", (String) session.get("string_key"));
                                  output.add("my_int_value", (int) session.get("int_key"));
                                }
                                Wrapper wrapper = (Wrapper)session.get("driver");
                                exports.MySecondKeyword = async (input, output, session) => {
                                  output.add('value', session.get('sharedKey'))
                                }

                                If you’re using a collection, see the method Arrays.copyOf to convert all of its content at once back to the original type.

                                You can then create a test plan using the “Session” control in order to pass the session information trough different Keywords. Note that we also included a keyword outside a session to see what’s append when no session exist but the keyword try to access it anyway:

                                A step session

                                Once you executed the plan you can see as a result that the session objects have been properly retrieved and displayed as output. The last keyword throws an error because it is outside a session and cannot retrieve the objects anymore as the session was closed:

                                A step execution, with a session

                                Storing by class name

                                You can also store your data using the class of the object. You will not need to cast it back but can only store one object per type :

                                  @Keyword(name="PutToSession")
                                  public void putToSession(){
                                    MyObject obj = new MyObject();
                                    session.put(obj);
                                  }

                                  this functionality is not available with the dotnet agent

                                  this functionality is not available with the node agent

                                  You can then access your data from another keyword without having to cast your object:

                                    @Keyword(name="GetFromSession")
                                    public void getFromSession() {
                                      MyObject obj = session.get(MyObject.class);
                                    }

                                    this functionality is not available with the dotnet agent

                                    this functionality is not available with the node agent

                                    Releasing session objects

                                    When putting objects in session, it can be useful to have a way to free any resources used by this objects. A typical example will be to quit a Selenium driver when this driver is not used anymore.

                                    This can be done by having the object in session implements Closable:

                                      public class DriverWrapper implements Closeable {
                                        final WebDriver driver;
                                        public DriverWrapper(WebDriver driver) {
                                          super();
                                          this.driver = driver;
                                        }
                                        @Override
                                        public void close() {
                                          driver.quit();
                                        }
                                        public WebDriver getDriver() {
                                          return driver;
                                        }
                                      }
                                      public class DriverWrapper : ICloseable
                                      {
                                        private IWebDriver Driver;
                                        public DriverWrapper(IWebDriver driver) {
                                          this.Driver = Driver;
                                        }
                                        public void Close() {
                                          Driver?.Quit();
                                        }
                                        public void GetDriver() {
                                          return Driver;
                                        }
                                      }

                                      Objects stored in the session are automatically disposed of when the session is released. The following disposal methods are supported:

                                      • [Symbol.dispose]() — preferred (ECMAScript explicit resource management)
                                      • [Symbol.asyncDispose]() — preferred for asynchronous disposal
                                      • .kill() — for Node.js child processes
                                      • .close() — for drivers, streams, database connections, etc.

                                      If a disposal method returns a promise (e.g. Symbol.asyncDispose or .close()), it is awaited before the session is released.

                                      exports.OpenBrowser = async (input, output, session) => {
                                        const browser = await chromium.launch()
                                      
                                        // Add the Playwright browser to the session so it is closed automatically (via its close() method) when the session is released.
                                        session.set('browser', browser)
                                      }
                                      
                                      exports.OpenConnection = async (input, output, session) => {
                                        const db = await connectToDatabase()
                                      
                                        // Add a wrapper object with a close() method to the session in order to disconnect the client when the session is released.
                                        session.set('db', {
                                          close() { db.disconnect() }
                                        })
                                      }
                                      
                                      exports.SpawnWorker = async (input, output, session) => {
                                        const proc = require('child_process').spawn('worker', [])
                                      
                                        // Add the child process directly to the session so it is closed automatically (via its kill() method) when the session is released.
                                        session.set('worker', proc)
                                      }

                                      When such an object is put into a session, its disposal method will be called when the session is released. For example, when executing a chrome scenario using our step-library-kw-selenium/ library:

                                      A step plan, executing a selenium test

                                      The first call to “Navigate_to” will succeed, but the second one will fail as the chrome driver is closed by then:

                                      An execution of a selenium test

                                      Keyword Proxy: plans as Code

                                      The Keyword Proxy is a powerful mechanism that allows developers to invoke keywords directly from within another keyword, enabling the creation of workflows directly in automation code

                                      The keyword proxy is currently only supported by the Java API.

                                      How it works

                                      The Keyword Proxy acts as an intermediary that enables a “parent” keyword to call one or more “child” keywords seamlessly. The main features are:

                                      • Automatically share the parent keyword’s context (Session, properties…) to the nested keyword calls
                                      • Manage how to propagate the keyword’s output (either by merging all individuals outputs or manually setting the final outputs)
                                      • Automatically creating measurements for all individual keywords calls

                                      Here is a sample to illustrate how it works

                                          /**
                                           * WorkflowAsCode is a Step's Keyword making use of the Keyword proxy to invoke other keywords and to create a 
                                           * workflow directly as code. As any other keywords, it can be called from a Step's plan, receiving inputs and returning outputs.
                                           */
                                          @Plan
                                          @Keyword
                                          public void WorkflowAsCode() {
                                              //Create a keyword proxy inheriting the current keyword context (i.e. session, properties...)
                                              //Pass true as 2nd parameter to instruct the proxy to merge all outputs to the current keyword's output, false otherwise
                                              KeywordProxy keywordProxy = new KeywordProxy(this, true);
                                              //Get the proxy for the class containing the keywords to be invoked
                                              KeywordExample proxy = keywordProxy.getProxy(KeywordExample.class);
                                              //Retrieves inputs passed by the plan calling the WorkflowAsCode keyword (defaulting to properties)
                                              String myInputString = getInputOrProperty("myInputString");
                                              // Call nested keywords by directly invoking the keyword method and passing inputs as method arguments
                                              proxy.myFirstKeyword(3, false, myInputString);
                                              // The output of the nested keyword calls can be retrieved with the getLastOutput function
                                              Output<JsonObject> lastOutput = keywordProxy.getLastOutput();
                                              if (lastOutput.getPayload().getBoolean("shouldFail")) {
                                                  output.appendError("The call to myFirstKeyword returned a failure message");
                                                  return;
                                              }
                                              // Call further nested keywords 
                                              proxy.mySecondKeyword();
                                              proxy.myThirdKeyword(List.of("value1", "value2"), Map.of("key1", "val1"));
                                          }
                                      

                                      Live Reporting

                                      Live Reporting allows you to stream data such as log files and measurements in real time while the keyword is running (as opposed to returning them only after the keyword has finished). Note that both ways to report results (“traditional” by adding them to the output, and live) are available at the same time – you can freely choose or combine them as appropriate for your use case.

                                      Live Reporting is currently only supported by the Java API.

                                      Sample usage

                                      Here is some example code to showcase how to create and use a streaming file upload:

                                      @Keyword /* Advanced error handling left out for the sake of readability */
                                      public void MyKeywordWithLiveReporting() throws IOException, InterruptedException {
                                      
                                          // This measure will be reported as soon as it is stopped
                                          liveReporting.measures.startMeasure("realtime_measure");
                                          
                                          // This could also be a file produced by something else, like a log file/stdout of another process;
                                          // We're directly populating it here for demo purposes
                                          Path logFile = Files.createTempFile("logfile", ".txt");
                                          StreamingUpload upload = null;
                                          try {
                                              upload = liveReporting.fileUploads.startTextFileUpload(logFile.toFile());
                                          } catch (QuotaExceededException e) {
                                              // Your KW should be prepared to handle at least "upload refused" exceptions due to server-side quota limits
                                              output.add("quota error", e.getMessage());
                                          }
                                      
                                          Files.writeString(logFile, "A line of text that can be streamed in realtime\n", StandardOpenOption.APPEND);
                                          Thread.sleep(1000);
                                          Files.writeString(logFile, "Another line.\n", StandardOpenOption.APPEND);
                                          // ... more keyword logic ...
                                      
                                          // Once finished, uploads MUST be signaled to be complete!
                                          if (upload != null) {
                                              try {
                                                  // signal that upload is finished, wait for server acknowledgement
                                                  StreamingResourceStatus uploadStatus = upload.complete(Duration.ofSeconds(5));
                                                  if (!uploadStatus.getTransferStatus().equals(StreamingResourceTransferStatus.COMPLETED)) {
                                                      output.add("warning - unexpected transfer status", uploadStatus.getTransferStatus().name());
                                                  }
                                              } catch (QuotaExceededException e) {
                                                  // Again, be prepared to at least handle aborted uploads due to quota restrictions
                                                  output.add("quota error", e.getMessage());
                                              } catch (TimeoutException | ExecutionException e) {
                                                  output.add("unexpected error", e.getMessage());
                                              }
                                          }
                                      
                                          // Live measures must indicate their own status, as the keyword is not finished yet and its status is not yet known
                                          liveReporting.measures.stopMeasure(Measure.Status.PASSED);
                                          // clean up our temporary demo file
                                          Files.deleteIfExists(logFile);
                                      }
                                      

                                      Please note the following important aspects when using Live Reporting for streaming files:

                                      1. All streamed uploads must explicitly signal completion. This is necessary because especially with files produced by other applications, it is impossible to automatically determine whether a file was completely written, or if there will be more data appended. The only reliable way is to make this information explicit – for example, if an external process wrote to a log file, and the process has finished executing, the file is guaranteed to have been completed. If keyword code does not explicitly signal completion of an upload, the upload will be terminated when the keyword finishes its execution, and the attachment will contain the data that has been transferred up to that point; however, the attachment will indicate that the data is potentially incomplete because it has not been properly signalled to be complete.
                                      2. To ensure robust keyword code, upload operations must be prepared to handle quota limits. These limits may cause uploads to be refused outright, or they may signal that not all data has been transmitted when attempting to complete an upload.

                                      The example code shown here uses the high level abstraction layer to leverage streaming uploads in a relatively straightforward fashion. Please see the API Javadoc for accessing lower-level abstraction layers and more advanced features.

                                      Live Metrics

                                      Metrics registered via output (see Metrics above) are only reported once the keyword completes. Use liveReporting.metrics to register metrics that the framework reports in real time while long-running keywords are executing.

                                      @Keyword
                                      public void BulkImport() {
                                          CounterMetric processed = liveReporting.metrics.registerCounter("records_processed");
                                          GaugeMetric batchSize   = liveReporting.metrics.registerGauge("batch_size");
                                          HistogramMetric latency  = liveReporting.metrics.registerHistogram("batch_latency_ms");
                                      
                                          for (Batch batch : getBatches()) {
                                              long t0 = System.currentTimeMillis();
                                              process(batch);
                                              processed.increment(batch.size());
                                              batchSize.observe(batch.size());
                                              latency.observe(System.currentTimeMillis() - t0);
                                              // The framework dispatches metric snapshots on its own interval — no flush call needed
                                          }
                                      }
                                      

                                      Labels are supported on all registration methods (see Labels above for semantics):

                                      CounterMetric errors = liveReporting.metrics.registerCounter("errors", Map.of("service", "payment"));
                                      GaugeMetric depth    = liveReporting.metrics.registerGauge("queue_depth", Map.of("region", "eu"));
                                      HistogramMetric rt   = liveReporting.metrics.registerHistogram("response_time_ms", Map.of("env", "prod"));
                                      

                                      To register a metric object you built yourself, use register:

                                      CounterMetric custom = new CounterMetric("retries", Map.of("type", "transient"));
                                      liveReporting.metrics.register(custom);
                                      

                                      For full details on configuration and the distinction between live and batch reporting, see the Live Reporting page.

                                      See Also

                                      • Keyword Development
                                      • Controls
                                      • Using variables in Plans
                                      • Automation Package Descriptor
                                      • Automation Package in Java
                                      • Home
                                      • Whats new?
                                      • Release Strategy
                                      • Set up
                                      • Administration
                                      • SaaS guide
                                      • User guide
                                      • Developer guide
                                        • Development Overview
                                        • Keyword Development
                                        • Keyword API
                                        • Step client API
                                        • REST API
                                        • Event Broker API
                                      • DevOps
                                      • AI
                                      • Plugins
                                      • Libraries
                                      Step Logo
                                        • Documentation
                                        • Tutorials
                                        • Blogs
                                        • Product
                                        • Home
                                        • Whats new?
                                        • Release Strategy
                                        • Set up
                                        • Administration
                                        • SaaS guide
                                        • User guide
                                        • Developer guide
                                          • Development Overview
                                          • Keyword Development
                                          • Keyword API
                                          • Step client API
                                          • REST API
                                          • Event Broker API
                                        • DevOps
                                        • AI
                                        • Plugins
                                        • Libraries