Skip to content

Script configuration

Each script has to define a configuration in the form of an interface that implements ScriptConfiguration. In this interface each defined function represents a configuration item. Cereal will read this interface and give the user the possibility to fill these in.

Best practices

  • Logical Grouping: Group related configuration items together by positioning order
  • Clear Names: Use descriptive names and detailed explanations for each configuration item
  • Defaults: Choose sensible defaults where applicable to simplify user experience
  • Required vs Optional: Clearly indicate which configuration values are required vs optional
  • Data Types: Choose appropriate types for your configuration values:
  • Use enums for choices among a fixed set of options
  • Use nullable types for truly optional values
  • Use the simplest type that meets requirements (Int vs Double)
  • Script Identification: Use isScriptIdentifier = true for the most distinctive configuration item so users can easily identify script instances

Default Values

You can provide default values for configuration items by using Kotlin's default interface method implementations. When a user creates a new configuration, these default values will be pre-populated in the configuration fields.

Supported types for default values

  • Boolean
  • String
  • Int
  • Float
  • Double
  • Enum (any enum type)

Note: Complex types like Proxy and RandomProxy do not support default values as they require runtime context.

Note: Secret does not support default values either, and this is deliberate rather than a limitation — a default returning a credential would be a hardcoded secret in your source. Ask the user for the credential instead.

Note: A complex list does not support default values or pre-seeded rows, and Cereal refuses to load a script that gives one a default implementation. A default that looks honoured but is not would be worse than a load failure.

Example with default values

interface MonitorUptimeConfiguration : ScriptConfiguration {

    @ScriptConfigurationItem(
        keyName = "website",
        name = "Website",
        description = "The website to monitor.",
        position = 1,
    )
    fun website(): String = "https://example.com"  // Default value

    @ScriptConfigurationItem(
        keyName = "checkInterval",
        name = "Check Interval (seconds)",
        description = "How often to check the website status.",
        position = 2,
    )
    fun checkInterval(): Int = 60  // Default: check every 60 seconds

    @ScriptConfigurationItem(
        keyName = "alertOnFailure",
        name = "Alert on Failure",
        description = "Whether to send an alert when the website is down.",
        position = 3,
    )
    fun alertOnFailure(): Boolean = true  // Default: alerts enabled

    @ScriptConfigurationItem(
        keyName = "alertLevel",
        name = "Alert Level",
        description = "The severity level for alerts.",
        position = 4,
    )
    fun alertLevel(): AlertLevel = AlertLevel.WARNING  // Default enum value
}

enum class AlertLevel {
    INFO,
    WARNING,
    CRITICAL
}

When a user opens the configuration screen for this script, the fields will be pre-filled with:

  • Website: https://example.com
  • Check Interval: 60
  • Alert on Failure: true (checked)
  • Alert Level: WARNING (selected in dropdown)

Example

Below an example of a script configuration that monitors the uptime of a website and needs a website url to monitor.

interface MonitorUptimeConfiguration : ScriptConfiguration {

    @ScriptConfigurationItem(
        keyName = "website",
        name = "Website",
        description = "The website to monitor.",
        position = 1,
        valuePerTask = false,
    )
    fun website(): String?

}

Each function must be annotated with a ScriptConfigurationItem. In this annotation additional information needed to render the configuration GUI for your script is defined. Lets go through the important lines in this code snippet:

interface MonitorUptimeConfiguration : ScriptConfiguration {

An interface, with a name you can decide yourself, must implement ScriptConfiguration.

@ScriptConfigurationItem(

Each function is annotated with ScriptConfigurationItem.

keyName = "website",

This value is used to persist the users' configuration. This value must be unique within all the defined script configuration items within the scripts' configuration. This value should stay the same in the releases of your script.

name = "Website",

The name of the field, shown to the user when configuring the script.

description = "The website to monitor.",

The description of the configuration item. This is shown to the user and should provide additional explanation on what to fill in.

position = 1,

The (optional) position of the configuration item in the scripts' configuration screen presented to the user. Items are sorted by position first, and items sharing the same position are then sorted alphabetically by their name.

Set position explicitly

When position is omitted, every item shares the same default, so the whole configuration falls back to alphabetical ordering by namenot the order in which you declared the functions. Set position on every item if the order matters to you.

valuePerTask = false,

Whether the script requires the user to input a single value for the entire script or a unique value for each task that's created.

fun website(): String?

Method name (decided by you) and the return type (nullable) String. The return type is used to determine what is expected as input from the user. Both nullable and non-nullable return types are supported. Making the return type nullable can be useful when a fallback is available in your script.

Supported return types

Next to the basic types like string and integer, additional return types can be used for a more complex situations. In this table you'll find all the supported types that can be used as return type of the configuration item function.

Type Description valuePerTask supported
String A text yes
Int A number value yes
Float A float value yes
Double A double value yes
Boolean true or false no
Enum A selection from a fixed set of constants no
List<T> A user-editable list of records, T being a ScriptConfigurationListItem interface you define no
Proxy A single proxy no (enabled by default)
RandomProxy Gives access to a proxy on demand no
Secret A credential, masked on input and on display yes

This list is exhaustive. Any other return type — including Long, Short, Byte, and ProxyGroup — is rejected when Cereal loads your script, and the script will not start.

Long is not supported

There is no long configuration type. If you need a value that exceeds Int, declare the item as a String and parse it in your script, or model it in a smaller unit (for example minutes instead of milliseconds) that fits in an Int.

For a List, the type argument must be either String or an interface extending ScriptConfigurationListItem (see complex lists). List<Int> and other element types are rejected.

How CSV cells are read

Per-task values are bulk-loaded from a CSV, as are the rows of a complex list. Both read cells by the same rules: the separator is detected from the header row (comma, semicolon or tab), a blank cell means "no value", a Boolean column accepts true/false, yes/no or 1/0 in any capitalisation, and an Enum column must match one of your constants exactly. Anything else is rejected with a message naming the row and column, rather than being read as false or left empty.

Secrets

Available since SDK 1.11.0

The Secret configuration type requires cereal-sdk 1.11.0 or later. Set your manifest's sdk_version to 1.11.0 (or higher) to use it.

When your script needs a credential from the user — an API key, a session token, a webhook secret — declare the configuration item as Secret instead of String:

interface MonitorUptimeConfiguration : ScriptConfiguration {

    @ScriptConfigurationItem(
        keyName = "apiKey",
        name = "API key",
        description = "The API key for the monitoring service.",
        position = 1,
    )
    fun apiKey(): Secret

    @ScriptConfigurationItem(
        keyName = "webhookSecret",
        name = "Webhook secret",
        description = "Optional secret used to sign outgoing webhooks.",
        position = 2,
    )
    fun webhookSecret(): Secret?

}

Declaring an item as Secret gives you three guarantees:

  • Masked input. The configuration field renders as dots rather than characters, with a reveal toggle that defaults to off so the user can deliberately check what they entered.
  • Masked display. The value renders as *** wherever Cereal summarises configuration — the task list and the custom dataset table — while still distinguishing a configured credential from an unconfigured one.
  • It doesn't survive toString(). The value cannot be accidentally interpolated into a status message, a log line, or a crash report.

Reading the value

Call reveal() to obtain the plaintext at execution time. It is deliberately a distinct verb, so you — or a reviewer — can find every place a credential is unwrapped with a single search:

override suspend fun execute(
    configuration: MonitorUptimeConfiguration,
    provider: ComponentProvider,
    statusUpdate: suspend (String) -> Unit,
): ExecutionResult {
    val client = MonitoringClient(token = configuration.apiKey().reveal())

    // Interpolating the secret itself is safe — this logs "Authenticating with ***"
    statusUpdate("Authenticating with ${configuration.apiKey()}")

    return ExecutionResult.Success("Done")
}

Per-task credentials

Secret supports valuePerTask = true, so each task can run with its own credential — one API key per account, for example. Users can bulk-load these from a CSV like any other per-task value, and the custom dataset table masks them.

@ScriptConfigurationItem(
    keyName = "apiKey",
    name = "API key",
    description = "The API key for this account.",
    valuePerTask = true,
)
fun apiKey(): Secret

Validating a credential's format

Secret combines with stateModifier, and your state modifier receives the entered value as a SecretScriptConfigValue, so you can validate the credential's format while the user is still on the configuration screen instead of failing 30 seconds into a run. See Advanced configuration.

Combining valuePerTask with a state modifier

On a per-task item, valueForKey returns a SequenceScriptConfigValue wrapping the individual SecretScriptConfigValues — exactly as it does for every other per-task type. A state modifier that casts straight to SecretScriptConfigValue therefore gets null, getError returns null, and your validation silently never fires. Unwrap the sequence first if you need to validate per-task credentials.

Migrating an existing item from String

If you have already shipped a script whose credential is an ordinary String, change the return type to Secret and keep the keyName the same. That is the whole change. Your existing users' stored credentials are preserved — the field is pre-filled and now masked, and nobody has to re-enter anything.

The coercion runs one way only. Changing a Secret item back to String does not restore the old behaviour, because that would take a value the user was told is protected and start printing it in the task list. If you genuinely need that, use a new keyName.

Rotate the credential after migrating

Migrating preserves the stored value, but it cannot retract what already leaked. A credential that previously lived in a String item may already sit in old log files, crash reports, or exported datasets. Migrating protects it going forward only — tell your users to rotate the credential, or rotate it yourself if it's yours.

What Secret does not do

Secret is a narrow guarantee, and building false confidence in a security feature is worse than not having it. To be explicit about the boundary:

  • It is not what encrypts the value. Every configuration value and every custom dataset value is already encrypted at rest under the user's key, whether or not it is a Secret. This type does not change the encryption path. What it controls is where a value is allowed to appear.
  • reveal() is unguarded. Nothing stops you passing the plaintext to a logger, a notification, or a third party. What the type prevents is the accidental leak — the interpolated status message, the whole-configuration debug dump — which is the failure that actually happens in practice. What your script does with the plaintext afterwards is your script's business.
  • Migrating from String cannot retract an earlier leak. See the warning above; rotate the credential.
  • Declaring too low an sdk_version is a load failure, not a silent leak. If you use Secret but declare an sdk_version below 1.11.0 in your manifest, older clients cannot resolve the return type at all and refuse to load your script. That is a visible failure rather than a quiet one — the credential is never rendered in clear text — but it does mean your script will not run, so keep sdk_version matched to your cereal-sdk dependency.

Unsupported combinations

Combination Why What happens
isScriptIdentifier = true on a Secret A masked identifier makes every instance of your script display identically; an unmasked one would defeat the display guarantee. Rejected at load time — the script will not start.
List<Secret> Use valuePerTask = true instead — it covers the "one credential per account" case better. Rejected at load time — the script will not start.
A default value for a Secret A default returning a credential is a hardcoded secret in source. Silently ignored. The default is not extracted and you get no error.

A default value on a Secret fails quietly

Unlike the other two, writing fun apiKey(): Secret = Secret("sk-…") does not stop your script loading. Cereal extracts defaults only for primitives, String and enums, so a Secret default is skipped with no error and the field simply starts empty. The hardcoded credential still sits in your source, where obfuscation will not remove it. Don't write one.

Proxy vs RandomProxy

Each of these types has its own use case. A Proxy gives your script access to a single proxy object containing an IP address and port, and optionally a username and password. The RandomProxy allows your script to request a random proxy when needed. This is particularly useful when, for example, you want to rotate the proxy for each subsequent request made to an external service.

Both the Proxy and the RandomProxy work based on the least-used proxy principle. This means that we track the usage of proxies in each task and provide a task with the least-used proxy at that moment, so you don't need to manage this manually.

Please note that it is inevitable that some proxies might end up being assigned to two or more different tasks if there are fewer proxies available than the number of created tasks.

At most one Proxy item per configuration

A configuration may declare only a single item returning Proxy. Declaring two or more makes the script invalid and Cereal will refuse to load it. There is no such limit on RandomProxy items.

Concurrency and tasks

A task is a single running instance of your script. Cereal decides how many tasks to create for a script based on its configuration:

  • If any configuration item returns Proxy or RandomProxy, or
  • If any configuration item has valuePerTask = true,

…the user controls the number of concurrent tasks. In all other cases, Cereal creates a single task.

Use valuePerTask = true when each parallel run should operate on its own input (e.g. one URL per task). Use a single Proxy/RandomProxy field to let users scale by proxy count.

A complex list does not fan out into tasks. It is one configuration value for one script run, and every task receives the whole list. Splitting the rows across tasks is your script's business; if you want one task per row, use valuePerTask = true instead. Both surfaces let a user enter many records, so see complex lists vs per-task values for which one a given case calls for.