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 = truefor 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
BooleanStringIntFloatDoubleEnum(any enum type)
Note: Complex types like
ProxyandRandomProxydo not support default values as they require runtime context.Note:
Secretdoes 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 name — not 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
Stringcannot retract an earlier leak. See the warning above; rotate the credential. - Declaring too low an
sdk_versionis a load failure, not a silent leak. If you useSecretbut declare ansdk_versionbelow1.11.0in 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 keepsdk_versionmatched to yourcereal-sdkdependency.
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
ProxyorRandomProxy, 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.