Scala 3 skill for Codex
Givens, enums and opaque types, migrated safely. Codex writes Scala 3 with the constructs the language now has: enums for ADTs, given and using for type classes, extension methods instead of implicit classes, and opaque types for domain IDs. For Scala 2.13 codebases it cross-builds, migrates leaf modules first, and lets the compiler's -source migration and -rewrite flags do the mechanical edits in reviewable commits.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- scala-3 Installs to ~/.agents/skills/scala-3/ for every project on this machine.
curl -fsSL codexskills.dev/cli.mjs -o codex-skills.mjsnode codex-skills.mjs install scala-3 The CLI also searches, updates and removes skills. Run it with no arguments to pick from a list. Needs Node 20.12+.
curl -fsSL codexskills.dev/install.sh \ | sh -s -- --project scala-3 Installs to .agents/skills/scala-3/ in the current repository. Commit it and everyone who runs Codex there gets it.
curl -fsSLO codexskills.dev/downloads/scala-3.zipunzip -o scala-3.zip -d ~/.agents/skills/ The archive holds the skill folder with its generated agents/openai.yaml. Unzip into .agents/skills/ to scope it to a repo.
& ([scriptblock]::Create((irm codexskills.dev/install.ps1))) scala-3 PowerShell 5.1 or 7. Installs to %USERPROFILE%\.agents\skills\. Add -Project for the current repo.
104-line POSIX script. No sudo, no telemetry. Read install.sh
Use it in Codex
Start a prompt with $scala-3, or pick it from /skills.
You can also just describe the task: Codex reads the description below and loads the playbook when your request matches.
What Codex reads before loading it
Writes idiomatic Scala 3 and migrates Scala 2.13 code - given/using instead of implicits, extension methods, enums and ADTs, opaque types, union types, derives clauses, significant indentation - with sbt, Mill or Scala CLI builds, scalafmt and scalafix, plus compiler rewrite flags for mechanical migration. Use when starting a Scala 3 module, porting a 2.13 codebase, cross-building a library, or decoding Scala 3 compiler errors about givens and implicits.
Use it when
- Starting a new Scala 3 module or service
- Porting Scala 2.13 code or cross-building a library for both
- Decoding errors about givens, implicits or ambiguous instances
- Modelling a domain with enums, opaque types and union types
Use something else when
- The problem is JVM garbage collection or heap sizing: use jvm-tuning
- The project is Kotlin rather than Scala: use kotlin-idioms
How to check the result
The build compiles with warnings as errors and tests pass on every cross Scala version, with scalafmt and MiMa checks clean for libraries.
The mistake it prevents
Using wildcard imports and expecting givens to come with them; Scala 3 needs import x.given, which is the root of many missing-instance errors after migration.
Good Scala 3 code says what it means with the new constructs (enums for ADTs, given
for type class instances, extension for added methods, opaque types for zero-cost
wrappers) and keeps implicit resolution small and local. Migrations are done with the
compiler’s rewrite tools in small, compiling steps, not by hand-editing thousands of
files.
Before changing code
- Read
AGENTS.md,build.sbt/build.mill/project.scala,project/plugins.sbt,.scalafmt.confand.scalafix.conf. - Record
scalaVersionand whether the build cross-compiles (crossScalaVersions). Scala 3.3.x is the LTS line; libraries usually publish against LTS so downstream users on any newer 3.x can consume them. Applications can track the latest 3.x. - Dependencies resolve from Maven Central; the first
sbt compileneeds network access and can take minutes. Ask before running it in a restricted sandbox.
Scala 2 to 3 translation table
| Scala 2 | Scala 3 |
|---|---|
implicit val ord: Ordering[User] = ... | given Ordering[User] = ... |
def f(x: Int)(implicit ec: ExecutionContext) | def f(x: Int)(using ec: ExecutionContext) |
implicitly[Show[A]] | summon[Show[A]] |
implicit class RichString(s: String) { def x = ... } | extension (s: String) def x = ... |
sealed trait Color; case object Red extends Color | enum Color { case Red, Green } |
implicit def a2b(a: A): B | given Conversion[A, B] = ... (plus import scala.language.implicitConversions at use sites) |
import cats.implicits._ | import cats.syntax.all.* (wildcard is *; givens need import x.given) |
List[_] | List[?] |
def main(args: Array[String]) in an object | @main def run(args: String*) = ... |
value class class Id(val v: Long) extends AnyVal | opaque type Id = Long |
macros (scala.reflect) | inline + quotes/splices; Scala 2 macros do not run in Scala 3 |
Idioms
ADTs with enums
enum PaymentEvent:
case Authorized(id: PaymentId, amount: BigDecimal)
case Captured(id: PaymentId)
case Failed(id: PaymentId, reason: String)
def describe(e: PaymentEvent): String = e match
case PaymentEvent.Authorized(id, amt) => s"authorized $amt on $id"
case PaymentEvent.Captured(id) => s"captured $id"
case PaymentEvent.Failed(id, why) => s"failed $id: $why"
The compiler warns on non-exhaustive matches; treat that warning as an error
(-Werror or -Xfatal-warnings depending on version) in CI.
Opaque types for domain IDs
object ids:
opaque type PaymentId = String
object PaymentId:
def apply(raw: String): Either[String, PaymentId] =
Either.cond(raw.startsWith("pay_"), raw, s"invalid payment id: $raw")
extension (id: PaymentId) def value: String = id
Outside ids, a PaymentId is not a String, at zero runtime cost.
Type classes with given/using and derives
trait Show[A]:
extension (a: A) def show: String
object Show:
given Show[Int] with
extension (a: Int) def show = a.toString
final case class User(name: String, age: Int) derives CanEqual
def render[A: Show](xs: List[A]): String = xs.map(_.show).mkString(", ")
- Put givens in the companion object of the type or the type class so they are found without imports.
- Name givens that are imported explicitly elsewhere (
given userOrdering: Ordering[User]); anonymous givens get compiler-generated names that change with the type. derivesworks with library type classes that providederived(e.g. circeCodec.AsObject,CanEqual).- Use
import x.given(orimport x.{given Ordering[?]}) to bring givens in; plain*does not import them.
Union types and errors
def parse(s: String): Int | ParseError is fine for local, closed alternatives. For
public APIs, prefer Either[Error, A] or an effect type the codebase already uses
(cats-effect IO, ZIO); do not mix three error styles in one module.
Syntax style
Scala 3 accepts both braces and significant indentation. Follow the repo’s choice; if
none exists, pick one and enforce it in .scalafmt.conf:
version = 3.8.3
runner.dialect = scala3
rewrite.scala3.convertToNewSyntax = true
rewrite.scala3.removeOptionalBraces = true # or false to keep braces
Use an exact scalafmt version that the repo pins; the line above is an example.
Migrating a Scala 2.13 codebase
- Get the 2.13 build green with
-Xsource:3(2.13.x accepts much Scala 3 syntax and warns about future breakage). Fix warnings. - Check every dependency has a
_3artifact. For 2.13-only libraries that do not use macros,("org" %% "lib" % "x").cross(CrossVersion.for3Use2_13)works; macro libraries need a Scala 3 version. - Cross-build:
crossScalaVersions := Seq("2.13.x", "3.3.x"), then migrate module by module, leaves first. - Use compiler rewrites:
scalacOptions ++= Seq("-source:3.4-migration", "-rewrite")for a compile, commit, then remove the flags.-new-syntax -rewriteor-indent -rewriteconvert syntax style. - Replace implicit conversions and implicit classes with
Conversiongivens and extensions; replace macros. - Run tests on both versions until the 2.13 leg is dropped.
Scala-version-specific source goes in src/main/scala-2.13/ and src/main/scala-3/;
sbt picks them up automatically.
Build tool notes
| Tool | Command |
|---|---|
| sbt | sbt compile, sbt test, sbt "testOnly *PaymentSpec", sbt +test (all cross versions) |
Scala CLI (the scala runner since 3.5) | scala run ., scala test ., directives //> using scala 3.3, //> using dep org::lib:1.2.3 |
| Mill | ./mill app.compile, ./mill app.test |
Run sbt as a long-lived server (sbt then commands, or sbt --client) to avoid paying
JVM startup on each step.
Gotchas
- Ambiguous given errors usually mean two instances are in scope (one imported, one in a companion); remove the import or prioritise with a lower-priority trait.
- “No given instance of type X was found” lists what it tried; look for a missing
import x.givenbefore writing a new instance. ==between unrelated types compiles unless strict equality is on (import scala.language.strictEqualitywithderives CanEqual); enable it for domain-heavy code.- Extension methods are found only if the extension is in scope or in the companion
of a type involved; a top-level
extensionin another package needs an import. - Opaque types are transparent inside their defining scope; validation in the
companion
applycan be bypassed there. -Werrorafter an upgrade surfaces new warnings in old code; budget for it.- Changing a public
givenname or an enum’s cases breaks binary compatibility; run MiMa (sbt mimaReportBinaryIssues) for libraries.
Verification
-
sbt +compile(or the equivalent) with warnings treated as errors in CI. -
sbt +testpasses on every cross version. -
sbt scalafmtCheckAlland, if configured,sbt "scalafixAll --check". - For libraries: MiMa against the last release.
- Report the Scala version(s), rewrite flags used and any dependencies still on
for3Use2_13.
What the zip contains
SKILL.md plus the interface file below. Nothing runs on install.
agents/openai.yaml
Gives the Codex app a display name, a one-line description and a starter prompt for this skill.
interface:
display_name: "Scala 3"
short_description: "Givens, enums and opaque types, migrated safely"
default_prompt: "$scala-3 Migrate this sbt project from Scala 2.13 to Scala 3" About this skill
Written and maintained by the Codex Skills editors to the same editorial standard as every skill here: a selective description, a real workflow, known pitfalls and a way to verify the work. First published , last updated . MIT licensed. Something wrong or out of date? Tell us.