Welcome to the new Golem Cloud Docs! 👋
File I/O in Scala Golem Agents

File I/O in Scala Golem Agents

Overview

Golem Scala agents are compiled to JavaScript via Scala.js and run in a QuickJS-based WASM runtime. The runtime provides node:fs for filesystem operations, accessible via Scala.js JavaScript interop. Standard JVM file I/O (java.io.File, java.nio.file.*) is not available.

To provision files into an agent's filesystem, See the golem-add-initial-files guide. To understand the full runtime environment, See the golem-js-runtime guide.

Setting Up the node:fs Facade

Define a Scala.js facade object for the node:fs module:

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
 
@js.native
@JSImport("node:fs", JSImport.Namespace)
private object Fs extends js.Object {
  def readFileSync(path: String, encoding: String): String = js.native
  def readFileSync(path: String): js.typedarray.Uint8Array = js.native
  def writeFileSync(path: String, data: String): Unit = js.native
  def existsSync(path: String): Boolean = js.native
  def readdirSync(path: String): js.Array[String] = js.native
  def appendFileSync(path: String, data: String): Unit = js.native
  def mkdirSync(path: String, options: js.Object): Unit = js.native
}

Important: WASI modules like node:fs are not available during the build-time pre-initialization (wizer) phase — they are only available at runtime. Use lazy val to defer initialization:

// ✅ CORRECT — lazy val defers import to first runtime use
private lazy val fs: Fs.type = Fs
 
// ❌ WRONG — top-level val triggers import during pre-initialization and fails
private val fs: Fs.type = Fs

Reading Files

Text Files

val content: String = Fs.readFileSync("/data/config.json", "utf-8")

Binary Files

val bytes: js.typedarray.Uint8Array = Fs.readFileSync("/data/image.png")

Writing Files

Only files provisioned with read-write permission (or files in non-provisioned paths) can be written to.

Fs.writeFileSync("/tmp/output.txt", "Hello, world!")

Checking File Existence

if (Fs.existsSync("/data/config.json")) {
  val content = Fs.readFileSync("/data/config.json", "utf-8")
}

Listing Directories

val files: js.Array[String] = Fs.readdirSync("/data")
files.foreach(println)

Complete Agent Example

import golem.runtime.annotations.{agentDefinition, agentImplementation}
import golem.BaseAgent
import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
import scala.concurrent.Future
 
@js.native
@JSImport("node:fs", JSImport.Namespace)
private object Fs extends js.Object {
  def readFileSync(path: String, encoding: String): String = js.native
  def appendFileSync(path: String, data: String): Unit = js.native
}
 
@agentDefinition()
trait FileReaderAgent extends BaseAgent {
  class Id(val name: String)
  def readGreeting(): Future[String]
  def writeLog(message: String): Future[Unit]
}
 
@agentImplementation()
final class FileReaderAgentImpl(private val name: String) extends FileReaderAgent {
 
  override def readGreeting(): Future[String] = Future.successful {
    Fs.readFileSync("/data/greeting.txt", "utf-8").trim
  }
 
  override def writeLog(message: String): Future[Unit] = Future.successful {
    Fs.appendFileSync("/tmp/agent.log", message + "\n")
  }
}

Key Constraints

  • Use node:fs via @JSImport — standard JVM file I/O (java.io.File, java.nio.file.*) does not work in Scala.js
  • Use lazy val or defer node:fs access to method bodies to avoid pre-initialization failures
  • Files provisioned via golem-add-initial-files with read-only permission cannot be written to
  • The filesystem is per-agent-instance — each agent has its own isolated filesystem
  • File changes within an agent are persistent across invocations (durable state)