Artifact Component
Available since SDK 1.10.0
The Artifact component requires cereal-sdk 1.10.0 or later. Set your manifest's sdk_version to 1.10.0
(or higher) to use it.
The Artifact component is your script's channel for output the user keeps: named blobs of binary data — a CSV export, a JSON report, a generated image — that the Cereal application persists against the current task and that the user can download from the app after the run finishes.
This is distinct from the other output channels:
- Logger and the
statusUpdatecallback — transient text for progress and diagnostics. - Notification — out-of-band alerts to the user or external services.
- Preference — small key/value state that the script itself reads back on a later run.
The emit method is a suspend function and must be called from a coroutine context (e.g., inside onStart,
execute, or onFinish).
Usage
suspend fun execute(provider: ComponentProvider) {
val artifact = provider.artifact()
val csv = buildString {
appendLine("sku,price")
appendLine("ABC-1,19.99")
}.toByteArray()
artifact.emit(
name = "results.csv",
bytes = csv,
mimeType = "text/csv", // optional; inferred from the name's extension when omitted
)
}
Parameters
| Parameter | Type | Description |
|---|---|---|
name |
String |
Display name for the artifact, also used as the default filename when the user downloads it. Include the extension you want (e.g. "results.csv"). |
bytes |
ByteArray |
The artifact content. Held in memory, so keep it reasonably sized. |
mimeType |
String? |
Optional MIME type (e.g. "text/csv", "application/json"). When omitted, the host infers it from the name's extension where possible. |
Semantics
- Append-only. Every call to
emitproduces a new, distinct artifact scoped to the current task. Emitting the samenametwice yields two artifacts — it never replaces a previous one. - Persisted. Emitted artifacts outlive the run and the application session; they remain available for download until the user deletes the owning task.
- In-memory payload. The content is passed as a
ByteArrayheld in memory, so this is suited to small, bounded outputs. Avoid emitting payloads large enough to exhaust the heap. - Throws on failure.
emitreturns once the artifact has been durably persisted by the host. If persistence fails the call throws, surfacing the failure to your script — the artifact is not stored.
Handling optional artifacts
An uncaught failure from emit terminates the run with an error, like any other thrown exception. If an artifact is
optional and should not fail the run, wrap the call yourself:
try {
provider.artifact().emit(name = "report.json", bytes = report)
} catch (e: Exception) {
provider.logger().info("Could not persist report artifact: ${e.message}")
}
Best Practices
- Give artifacts descriptive names with the correct extension so the user gets a sensible download filename.
- Keep payloads small and bounded — the content is held in memory.
- Use artifacts for results the user wants to keep; use the Logger for progress and diagnostics.
Testing
The test utilities back the artifact channel with an in-memory RecordingArtifactComponent, which records every
emission in its emitted list, in emission order, as EmittedArtifact(name, mimeType, bytes).
TestComponentProviderFactory creates that recorder internally and does not expose it, so to assert on what your
script emitted, supply your own recorder through a factory that delegates everything else to the default one:
val artifacts = RecordingArtifactComponent()
val componentProviderFactory = object : ComponentProviderFactory {
private val delegate = TestComponentProviderFactory().create()
override fun create(): ComponentProvider =
object : ComponentProvider by delegate {
override fun artifact(): ArtifactComponent = artifacts
}
}
scriptRunner.run(configuration, componentProviderFactory)
assertEquals("results.csv", artifacts.emitted.single().name)
assertEquals("text/csv", artifacts.emitted.single().mimeType)
The same pattern works for any other component you want to observe: the bundled logger and notification components write to the console rather than to an inspectable list, so substitute your own implementation when you need to assert on their input. See Testing for the full test setup.