Skip to content

Custom Dialects

Starlark is a configuration language used by many build systems and tools. Each tool extends Starlark with its own builtin functions, types, and globals. This creates a challenge for tooling: how do you provide accurate completions, hover documentation, and diagnostics when different files use different builtins?

skyls solves this with custom dialect support. You can configure which files use which dialect, and provide builtin definitions for each dialect.

The term “dialect” describes how different tools extend vanilla Starlark with custom builtins, rules, and behaviors. This is not an official Starlark term - it is how sky and other tools describe Starlark variations.

If you come from the Python world, dialects are similar to how different frameworks and implementations extend or modify core Python:

Python ConceptStarlark ParallelDescription
CPython vs PyPy vs MicroPythonstarlark-go vs starlark-rust vs starlark-javaSame language, different interpreters with varying features
Django’s template languageBazel’s BUILD filesPython-like but with restrictions and custom builtins
Jinja2 templatesTilt’s Tiltfiles”Python-like but not Python” - familiar syntax, different capabilities
NumPy’s array syntax extensionsBuck2’s record and enum typesImplementation-specific extensions beyond the core spec
Flask vs Django vs FastAPIBazel vs Buck2 vs TiltSame language, different frameworks with different builtins

Every Starlark dialect builds on layers:

┌─────────────────────────────────────────────────────────────────┐
│ Tool-Specific Builtins │
│ (docker_build, cc_library, cxx_library, core.workflow, etc.) │
├─────────────────────────────────────────────────────────────────┤
│ Implementation Extensions │
│ (record, enum, union types - starlark-rust) │
├─────────────────────────────────────────────────────────────────┤
│ Core Starlark Builtins │
│ (len, range, str, dict, list, print, getattr, hasattr) │
├─────────────────────────────────────────────────────────────────┤
│ Starlark Language Spec │
│ (syntax, semantics, data types, control flow) │
└─────────────────────────────────────────────────────────────────┘

Vanilla Starlark (the official specification) provides:

# Core data types
numbers = [1, 2, 3]
mapping = {"key": "value"}
text = "hello"
# Core builtins
length = len(numbers) # 3
items = range(10) # [0, 1, 2, ..., 9]
joined = ", ".join(["a", "b"]) # "a, b"
# Functions and control flow
def greet(name):
if name:
return "Hello, " + name
return "Hello, World"
# Module loading
load("//lib:utils.star", "helper")

Bazel extends this with build-system concepts:

# Bazel-specific: rule definitions
cc_library(
name = "mylib",
srcs = ["lib.cc"],
hdrs = ["lib.h"],
deps = ["//other:lib"],
)
# Bazel-specific: native module in .bzl files
def _my_rule_impl(ctx):
# ctx is a Bazel-specific object
output = ctx.actions.declare_file(ctx.label.name + ".out")
ctx.actions.run(
outputs = [output],
inputs = ctx.files.srcs,
executable = ctx.executable._tool,
)
return [DefaultInfo(files = depset([output]))]
# Bazel-specific: rule() and provider()
MyInfo = provider(fields = ["value"])
my_rule = rule(implementation = _my_rule_impl, attrs = {...})

Buck2 has similar concepts but different names and additional type features:

# Buck2-specific: different rule names
cxx_library(
name = "mylib",
srcs = ["lib.cpp"],
headers = ["lib.h"], # Note: 'headers' not 'hdrs'
)
# Buck2-specific: record and enum types (starlark-rust extensions)
BuildConfig = record(
name = str,
srcs = list[str],
debug = field(bool, False),
)
Status = enum("pending", "running", "complete")
# Buck2-specific: BXL scripts for build graph queries
def _query_impl(ctx):
deps = ctx.cquery().deps(ctx.cli_args.target)
ctx.output.print("Dependencies: {}".format(len(deps)))

Tilt extends for Kubernetes development:

# Tilt-specific: container and K8s functions
docker_build(
'myapp-image',
context='.',
dockerfile='Dockerfile',
live_update=[
sync('./src', '/app/src'),
run('pip install -r requirements.txt', trigger=['requirements.txt']),
],
)
k8s_yaml('k8s/deployment.yaml')
k8s_resource('myapp', port_forwards='8080:8080')
# Tilt-specific: local development utilities
local_resource(
'compile',
cmd='make build',
deps=['./src'],
)

Each tool has domain-specific needs that core Starlark cannot address:

ToolDomainWhy Custom Builtins?
BazelBuild systemsNeeds rules, actions, providers, dependency graphs
Buck2Build systemsSimilar to Bazel, plus BXL for scripting the build graph
TiltK8s developmentNeeds container builds, K8s resources, live updates
CopybaraCode migrationNeeds git operations, transformations, workflows
KurtosisTest environmentsNeeds service orchestration, networking, plans
yttYAML templatingNeeds YAML-specific operations, overlays

The challenge for tooling is identifying which dialect a file uses. skyls uses several signals:

  1. Filename patterns: BUILD files are Bazel, Tiltfile is Tilt
  2. Workspace markers: Presence of WORKSPACE, .buckconfig, etc.
  3. Explicit configuration: The .starlark/config.json file
  4. File extensions: .bzl vs .star vs .bara.sky

The following table shows which builtins are available in each dialect:

DialectToolFilesExample Builtins
BazelBazel build systemBUILD, *.bzl, WORKSPACE, MODULE.bazelcc_library, java_binary, rule, provider
Buck2Buck2 build systemBUCK, *.bzlcxx_library, genrule, bxl functions
TiltTilt local devTiltfile, *.stardocker_build, k8s_yaml, local_resource
CopybaraCopybara migration*.bara.skycore.workflow, git.origin, transformations
KurtosisTest environments*.starplan.add_service, ServiceConfig, plan.wait
CustomYour own toolsAny patternYour DSL functions

Without dialect awareness, skyls would report errors like undefined: docker_build when editing a Tiltfile, because it does not know about Tilt’s builtins.

Available everywhere - these builtins are part of the Starlark specification:

# Type constructors
bool(x) # Convert to boolean
dict(**kwargs) # Create dictionary
int(x) # Convert to integer
list(iterable) # Create list
str(x) # Convert to string
tuple(iterable) # Create tuple
# Sequence operations
len(x) # Length of sequence
range(stop) # Integer sequence
range(start, stop) # Integer sequence with start
sorted(x) # Sorted copy
reversed(x) # Reversed iterator
# Dictionary operations
{}.get(key, default) # Get with default
{}.items() # Key-value pairs
{}.keys() # Keys iterator
{}.values() # Values iterator
{}.update(other) # Merge dictionaries
# String operations
"".format(*args) # String formatting
"".join(iterable) # Join strings
"".split(sep) # Split string
# Attribute access
getattr(obj, name) # Get attribute
hasattr(obj, name) # Check attribute exists
dir(obj) # List attributes
# Other
print(*args) # Output (implementation-defined)
type(x) # Type name as string
zip(a, b) # Pair up iterables
enumerate(x) # Index-value pairs
any(iterable) # True if any truthy
all(iterable) # True if all truthy

Dialect support is configured via .starlark/config.json in your workspace root.

skyls searches for configuration in this order:

  1. CLI flag: --config path/to/config.json (planned)
  2. Environment: STARLARK_CONFIG=/path/to/config.json (planned)
  3. .starlark/config.json (walking up from the file being edited)
  4. starlark.config.json in workspace root
  5. Auto-detection based on workspace markers (WORKSPACE, .buckconfig, Tiltfile)

For simple cases, specify just the dialect:

{
"$schema": "https://sky.dev/schemas/starlark-config.json",
"version": 1,
"dialect": "bazel"
}

This tells skyls to use Bazel builtins for all Starlark files in the workspace.

For complex workspaces with multiple dialects:

{
"$schema": "https://sky.dev/schemas/starlark-config.json",
"version": 1,
"rules": [
{
"files": ["Tiltfile", "tilt_modules/**/*.star"],
"dialect": "tilt"
},
{
"files": ["*.bara.sky", "copy.bara.sky"],
"dialect": "copybara"
},
{
"files": ["**/*.bzl"],
"dialect": "bazel-bzl"
},
{
"files": ["BUILD", "BUILD.bazel", "**/BUILD", "**/BUILD.bazel"],
"dialect": "bazel-build"
},
{
"files": ["**/*.star"],
"dialect": "starlark"
}
],
"dialects": {
"tilt": {
"builtins": [
".starlark/builtins/tilt.builtins.json"
],
"extends": "starlark"
},
"copybara": {
"builtins": [
".starlark/builtins/copybara.builtins.json"
],
"extends": "starlark"
},
"bazel-build": {
"builtins": [],
"extends": "starlark"
},
"bazel-bzl": {
"builtins": [],
"extends": "bazel-build"
}
},
"settings": {
"reportUndefinedNames": true,
"reportUnusedBindings": true,
"checkLoadStatements": true
}
}
FieldTypeRequiredDescription
$schemastringNoJSON Schema URL for validation
versionnumberYesSchema version (currently 1)
dialectstringNoDefault dialect for all files
rulesarrayNoFile pattern to dialect mapping
dialectsobjectNoCustom dialect definitions
settingsobjectNoAnalysis settings

Rules map file patterns to dialects. The first matching rule wins.

{
"rules": [
{
"files": ["Tiltfile", "tilt_modules/**/*.star"],
"dialect": "tilt"
}
]
}
FieldTypeDescription
filesstring[]Glob patterns to match (relative to config file)
dialectstringDialect ID to apply
PatternMatches
*Any characters except /
**Any characters including / (recursive)
?Any single character
[abc]Character class
{a,b}Alternatives

Examples:

{
"rules": [
{"files": ["BUILD"], "dialect": "bazel"},
{"files": ["BUILD.bazel"], "dialect": "bazel"},
{"files": ["**/BUILD"], "dialect": "bazel"},
{"files": ["**/*.bzl"], "dialect": "bazel"},
{"files": ["src/**/*.star"], "dialect": "custom"},
{"files": ["tests/**/*_test.star"], "dialect": "custom"}
]
}

Define custom dialects with builtin sources and inheritance:

{
"dialects": {
"my-dialect": {
"builtins": [
".starlark/builtins/core.builtins.json",
".starlark/builtins/extensions.builtins.json",
"https://example.com/builtins/v1.json"
],
"extends": "starlark"
}
}
}
FieldTypeDescription
builtinsstring[]Paths or URLs to builtin definition files
extendsstringParent dialect to inherit builtins from

Builtins can be loaded from:

  1. Local files: Relative to config file or absolute paths
  2. Remote URLs: HTTPS URLs (planned, not yet implemented)
  3. Embedded data: Built-in definitions for Bazel, Buck2 (automatic)

Control analysis behavior:

{
"settings": {
"reportUndefinedNames": true,
"reportUnusedBindings": true,
"checkLoadStatements": true
}
}
SettingTypeDefaultDescription
reportUndefinedNamesbooleantrueReport undefined variable/function references
reportUnusedBindingsbooleantrueReport unused local variables
checkLoadStatementsbooleanfalseValidate load statement targets exist

Dialects can extend other dialects using the extends field. This creates a chain of builtin providers:

starlark (core builtins)
|
+-- bazel-build (BUILD file builtins)
| |
| +-- bazel-bzl (.bzl file builtins)
|
+-- tilt (Tilt builtins)
|
+-- copybara (Copybara builtins)

When resolving builtins:

  1. Start with the specified dialect’s builtins
  2. Walk up the inheritance chain
  3. Merge all builtins (child overrides parent on collision)

Example:

{
"dialects": {
"internal-build": {
"builtins": [".starlark/builtins/internal.builtins.json"],
"extends": "bazel-build"
}
}
}

The internal-build dialect gets:

  1. Core Starlark builtins (len, range, str, …)
  2. Bazel BUILD builtins (cc_library, glob, …)
  3. Internal builtins (your custom rules)

skyls supports three formats for defining builtins:

FormatExtensionBest For
JSON.builtins.jsonEasy authoring, human-readable
Textproto.builtins.textprotostarpls compatibility, proto validation
Python Stub.builtins.pyiTilt LSP compatibility, familiar syntax

See Builtin Formats Reference for complete format specifications.

{
"$schema": "https://sky.dev/schemas/starlark-builtins.json",
"version": 1,
"name": "my-dialect",
"description": "Custom Starlark dialect for my tool",
"functions": [
{
"name": "my_function",
"doc": "Does something useful.\n\nThis is a longer description.",
"params": [
{"name": "name", "type": "string", "doc": "Resource name", "required": true},
{"name": "config", "type": "dict", "doc": "Configuration options", "default": "{}"}
],
"return_type": "MyResult"
}
],
"types": [
{
"name": "MyResult",
"doc": "Result of my_function",
"fields": [
{"name": "status", "type": "string", "doc": "Success or failure"}
]
}
],
"globals": [
{"name": "VERSION", "type": "string", "doc": "Tool version"}
]
}

Recommended directory layout for dialect configuration:

  • Directoryproject/
    • Directory.starlark/
      • config.json
      • Directorybuiltins/
        • tilt.builtins.json
        • copybara.builtins.json
        • internal.builtins.json
    • BUILD
    • Tiltfile
    • copy.bara.sky
    • Directorysrc/
      • lib.bzl

For monorepos with nested configurations:

  • Directorymonorepo/
    • Directory.starlark/
      • config.json (root config)
    • Directoryprojects/
      • Directoryfrontend/
        • Directory.starlark/
          • config.json (overrides for frontend)
        • BUILD
      • Directorybackend/
        • BUILD
  1. Create the configuration directory

    Terminal window
    mkdir -p .starlark/builtins
  2. Create the config file

    Create .starlark/config.json:

    {
    "$schema": "https://sky.dev/schemas/starlark-config.json",
    "version": 1,
    "rules": [
    {
    "files": ["Tiltfile", "**/*.tilt.star"],
    "dialect": "tilt"
    }
    ],
    "dialects": {
    "tilt": {
    "builtins": [".starlark/builtins/tilt.builtins.json"],
    "extends": "starlark"
    }
    }
    }
  3. Create the builtins file

    Create .starlark/builtins/tilt.builtins.json:

    {
    "$schema": "https://sky.dev/schemas/starlark-builtins.json",
    "version": 1,
    "name": "tilt",
    "functions": [
    {
    "name": "docker_build",
    "doc": "Build a Docker image from a Dockerfile.",
    "params": [
    {"name": "ref", "type": "string", "required": true, "doc": "Image reference"},
    {"name": "context", "type": "string", "default": "'.'", "doc": "Build context path"},
    {"name": "dockerfile", "type": "string", "default": "'Dockerfile'", "doc": "Dockerfile path"}
    ],
    "return_type": "None"
    },
    {
    "name": "k8s_yaml",
    "doc": "Deploy Kubernetes YAML manifests.",
    "params": [
    {"name": "yaml", "type": "string | list[string]", "required": true, "doc": "YAML content or paths"}
    ],
    "return_type": "None"
    }
    ]
    }
  4. Verify the configuration

    Open your Tiltfile in an editor with skyls. You should see:

    • No “undefined” errors for docker_build and k8s_yaml
    • Completions for these functions
    • Hover documentation
  5. Add more builtins as needed

    When you use a new Tilt function, add it to the builtins file.

Complete configuration for Tilt development:

{
"$schema": "https://sky.dev/schemas/starlark-config.json",
"version": 1,
"rules": [
{
"files": ["Tiltfile", "tilt_modules/**/*.star"],
"dialect": "tilt"
}
],
"dialects": {
"tilt": {
"builtins": [".starlark/builtins/tilt.builtins.json"],
"extends": "starlark"
}
}
}
{
"$schema": "https://sky.dev/schemas/starlark-config.json",
"version": 1,
"rules": [
{
"files": ["*.bara.sky", "copy.bara.sky"],
"dialect": "copybara"
}
],
"dialects": {
"copybara": {
"builtins": [".starlark/builtins/copybara.builtins.json"],
"extends": "starlark"
}
}
}

For a monorepo with Bazel, Tilt, and custom tools:

{
"$schema": "https://sky.dev/schemas/starlark-config.json",
"version": 1,
"rules": [
{"files": ["Tiltfile"], "dialect": "tilt"},
{"files": ["tools/codegen/**/*.star"], "dialect": "codegen"},
{"files": ["BUILD", "BUILD.bazel", "**/BUILD", "**/BUILD.bazel"], "dialect": "bazel-build"},
{"files": ["**/*.bzl"], "dialect": "bazel-bzl"},
{"files": ["**/*.star"], "dialect": "starlark"}
],
"dialects": {
"tilt": {
"builtins": [".starlark/builtins/tilt.builtins.json"],
"extends": "starlark"
},
"codegen": {
"builtins": [".starlark/builtins/codegen.builtins.json"],
"extends": "starlark"
},
"bazel-build": {
"builtins": [".starlark/builtins/bazel-custom-rules.builtins.json"],
"extends": "starlark"
},
"bazel-bzl": {
"builtins": [],
"extends": "bazel-build"
}
}
}

When multiple builtin sources define the same symbol, the resolution order is:

  1. Local builtins (from dialects[name].builtins) - highest priority
  2. Inherited builtins (from extends chain)
  3. Core builtins (built into skyls) - lowest priority

Within the builtins array, later files override earlier ones:

{
"builtins": [
"base.builtins.json", // Loaded first
"overrides.builtins.json" // Overrides 'base' on conflict
]
}

When no config file is found, skyls auto-detects the dialect based on workspace markers:

Marker FileDetected Dialect
WORKSPACE or WORKSPACE.bazelbazel
MODULE.bazelbazel
.buckconfigbuck2
Tiltfiletilt
(none)starlark

Auto-detection uses built-in builtins for standard dialects. For custom dialects, create a config file.

Check the file location and name:

  • .starlark/config.json (preferred)
  • starlark.config.json (root-level alternative)

Verify with:

Terminal window
ls -la .starlark/
cat .starlark/config.json
  1. Check paths are relative to the config file
  2. Verify JSON syntax:
    Terminal window
    python3 -m json.tool .starlark/builtins/my.builtins.json
  3. Check file extension is .builtins.json, .builtins.textproto, or .builtins.pyi

Check rule order - first match wins:

{
"rules": [
{"files": ["**/*.star"], "dialect": "starlark"}, // Matches everything!
{"files": ["Tiltfile"], "dialect": "tilt"} // Never reached
]
}

Fix by putting specific patterns first:

{
"rules": [
{"files": ["Tiltfile"], "dialect": "tilt"}, // Specific first
{"files": ["**/*.star"], "dialect": "starlark"} // General last
]
}
  1. Ensure the function is defined in builtins
  2. Check the file matches a rule for the correct dialect
  3. Verify the config file is valid JSON
  4. Restart the language server