This document provides essential information for AI coding agents working on the Kestra codebase.
IMPORTANT — READ FIRST
- Act as a Senior Software Engineer and Software Architect. Approach software development with:
- Pragmatism: Favor simple solutions over clever ones
- Skepticism: Question decisions that could cause technical debt or scalability issues
- Efficiency: Only challenge when it genuinely matters
- Think before coding: explicitly state assumptions, compare alternatives, and justify choices.
- Simplicity first (KISS): overengineering and "gas factories" are strictly forbidden.
- Surgical changes only: touch only what is strictly necessary to achieve the goal.
- Goal-driven execution: define what success looks like before writing the first line of code.
- Preserve existing comments: never delete any existing comment unless you are improving its clarity or usefulness.
- Keep comments short and only where they earn their place: a comment you write should be one sentence, or two at most when the why genuinely needs it (a non-obvious constraint, a workaround, a subtle ordering or concurrency requirement). Do not comment obvious code — no restating what the next line plainly says (
// increment the counter), no narrating a self-explanatory getter, loop, or well-named call. If the code is readable, the comment is noise; if it isn't, prefer making the code clearer over explaining it. - Write clear, maintainable, and well-documented code
- Build & test are mandatory
Monorepo built with Java (backend) and Vue (frontend), using Gradle as the build system.
- Backend: Java 25, Micronaut Framework, Lombok
- Frontend: Vue 3, TypeScript, Vite, Element Plus, Pinia
- Build: Gradle 8.x with multi-project structure (77 submodules)
- Testing: JUnit 5, Mockito, AssertJ, Vitest, Playwright
DO: Use constructor injection with final fields.
@Singleton
public class MyService {
private final SomeDependency dependency;
@Inject
public MyService(SomeDependency dependency) {
this.dependency = Objects.requireNonNull(dependency);
}
}DON'T: Use field injection (@Inject on fields directly). Always prefer constructor injection.
// 1. Package declaration and imports
// 2. Class-level annotations (@Slf4j, @Singleton, etc.)
// 3. Class declaration with Javadoc
// 4. Static constants (UPPER_SNAKE_CASE)
// 5. Injected fields (@Inject)
// 6. Constructors
// 7. Public methods
// 8. Protected methods
// 9. Private methods
// 10. Inner classes/records- Micronaut:
@Singleton,@Inject,@Controller,@Replaces,@Requires - Validation:
@Valid,@NotNull,@Nullable - Lombok:
@Slf4j,@Getter,@NoArgsConstructor,@AllArgsConstructor - Use
@Builderfor complex object creation
DO:
- Use specific exception types — extend
KestraExceptionorKestraRuntimeException - Use
Optional<T>for potentially absent returned values - Return empty collections (e.g.,
List.of(),Collections.emptyList()) for absent values - Use try-with-resources for resource management
- Log errors before re-throwing:
log.error("message", exception) - Write exception messages as plain, complete sentences that state the fact and the actionable detail — build them with
String.formatted()/String.format(), not string concatenation or em dashes, e.g."Cannot acquire lock on asset '%s': already locked by '%s' until %s.".formatted(id, owner, until)
DON'T: Use generic Exception. Don't return null for collections. Don't write terse or telegraphic exception messages (e.g. dropping articles/verbs) or string-concatenate message parts.
- Use java records for simple data carriers
- Follow Java naming-convention best practices for Classes, Methods, Variables, Constants.
- Boolean methods: Start with
is,has,should,can(e.g.,isReadOnly()).
- Use 4-space indentation (configured in .editorconfig)
- UTF-8 encoding with LF line endings
- No trailing whitespace
- Mark utility classes as
finalwith a private constructor - Use static methods only
- Use existing utility classes (e.g.,
ListUtils,MapUtils) instead of creating new ones (io.kestra.core.utils.*)
MANDATORY — never hand-roll Pebble delimiter detection. Pebble has two block delimiter pairs — print blocks ({{ ... }}) and execute/statement blocks ({% ... %}) — and code that only checks for {{/}} silently misses {%/%} blocks. Use io.kestra.core.utils.PebbleUtil (containsOpeningBlockDelimiter, startsWithOpeningBlockDelimiter, endsWithClosingBlockDelimiter, openingBlockDelimiters()/closingBlockDelimiters()) instead of writing a new delimiter regex or literal — it derives the delimiter pairs from Pebble's own Syntax.Builder defaults, so it never drifts from what Pebble actually parses.
- Use enums for fixed sets of constants, including internal fields not exposed over the API — prefer a typed enum over a raw
String/intwhenever the value is drawn from a closed set of known cases, even if the set may only ever have a couple of members - Use
@JsonValuefor custom serialization if needed - Use
UNKNOWNenum value for unknown cases in deserialization - Compare Constants From The Left (a.k.a., Yoda conditions)
- Use a static
fromStringmethod for case-insensitive lookups usingEnumsclass.
e.g.:
public enum MyEnum {
VALUE_ONE,
VALUE_TWO,
UNKNOWN;
@JsonCreator
public static ResourceType fromString(final String value) {
return Enums.getForNameIgnoreCase(value, MyEnum.class, UNKNOWN);
}
}- Javadoc for all public classes and methods - be concise
- Use
@param,@return,@throwsappropriately - Use
{@inheritDoc}for inherited methods - Include usage examples for complex methods
- Put classes used by only controllers in the webserver module (not core)
- No business code/rule inside controllers - instead use a Service class
- All APIs must return a valid JSON object
- APIs should not return a response being a JSON array which cannot be evolved in a backwards-compatible way
- Unit tests must assert that a user can only access a given API if authorized to do so, and that access is denied otherwise
- APIs must be documented with OpenAPI annotations
- Use DTOs for requests/responses
- Always validate input parameters with
@Valid - Use
@ExecuteOn(TaskExecutors.IO)for blocking operations - Return meaningful error responses in controllers
- Never depend on repositories for code called by the workers - instead use MetaStore/StateStore facades
- Run the
H2RunnerTestwhenever you update part of the executor
DO:
- Place tests in same package structure as source code
- Simple unit test with mocks over complex integration tests when possible
- Add // Given-When-Then comments for clarity
- Test method naming:
should<ExpectedBehavior>When<ConditionOrAction>(also...Given<Input>,...For<Condition>,...If<Condition>), e.g.shouldThrowExceptionWhenDividingByZero() - Use
@MicronautTestfor tests that require Micronaut beans - Use
@KestraTestfor tests that require running Kestra services (e.g., Executor, Scheduler)
@KestraTest
class ServiceTest {
@Inject
private ServiceClass service;
@Test
void shouldPerformActionWhenCondition() {
// Given (setup)
// When (action)
// Then (assertions)
assertThat(result).isNotNull();
}
}DON'T: Use Nested classes for test organization. Avoid complex test hierarchies.
Assertions:
- Use AssertJ:
assertThat().isEqualTo(),assertThat().isNotNull(),assertThatThrownBy(),assertThatObject() - Prefer descriptive assertion methods
- Use
@MockBeanfor mocking dependencies
Test Categories:
- Unit tests: Fast, isolated, no external dependencies
- Integration tests: Test component interaction, use
@Tag("integration") - Flaky tests: Use
@Tag("flaky")for unreliable tests
- Unit tests with Vitest and
@vue/test-utils - E2E tests with Playwright
- Storybook component tests
- Use JSdom environment for DOM testing
- Prefer Storybook component tests over Vitest unit tests whenever possible — components render through their real story setup (props, slots, design-system deps) instead of being stubbed out, catching regressions unit mocks miss. Fall back to a Vitest unit test only when the logic under test isn't component-rendering behavior (e.g. a pure helper/composable) or no story exists and adding one isn't practical.
The full UI design-system rules, component catalogue, token reference, and frontend best practices live in ui/AGENTS.md. That file is auto-loaded by AI coding agents whenever work happens under ui/ in OSS or ui-ee/ in Enterprise edition, and should be consulted (and kept up to date) for any frontend change.
@ui/AGENTS.md
File Organization:
- Use 2-space indentation for Vue, JSON, YAML, CSS
- Use 4-space indentation for JavaScript/TypeScript
- Follow Vue 3 Composition API patterns
- Organize imports: Vue/framework → third-party → local modules
Naming Conventions:
- Components:
PascalCasefiles (e.g.,MyComponent.vue) - Variables/functions:
camelCase - Constants:
UPPER_SNAKE_CASE - CSS classes: Follow Element Plus conventions
TypeScript:
- Use strict TypeScript configuration
- Prefer type definitions over
any - Use interfaces for object shapes
- Use enums for fixed sets of values
# Clean build
./gradlew clean
# Full build (includes tests)
./gradlew build
# Build without tests (faster)
./gradlew build -x test -x integrationTest -x testCodeCoverageReport --refresh-dependencies --no-daemon --parallel# Run all tests (excludes flaky tests)
./gradlew test
# Run only unit tests (fastest)
./gradlew unitTest
# Run integration tests
./gradlew integrationTest
# Run flaky tests (separate from build)
./gradlew flakyTest
# Run tests for specific module
./gradlew :core:test
# Run single test class
./gradlew :module-name:test --tests "ClassName"
# Run single test method
./gradlew :module-name:test --tests "ClassName.methodName"
# After running tests: generate a markdown summary of failures only
npx --yes @kestra-io/kestra-devtools generateTestReportSummary --only-errors $(pwd)cd ui
# Install dependencies
npm install
# Development server
npm run dev
# Type checking
npm run check:types
# Build for production
npm run build
# Run tests
npm run test:all # All tests with coverage
npm run test:unit # Unit tests only
npm run test:storybook # Storybook tests
npm run test:e2e # End-to-end tests
# Linting
npm run lint # Fix linting issues
npm run test:lint # Check linting only
# Storybook
npm run storybook # Development
npm run build-storybook # Build- Start/stop backends:
# Start databases with Docker Compose
docker compose -f docker-compose-ci.yml up
# Stop databases with Docker Compose
docker compose -f docker-compose-ci.yml down- Access application: http://localhost:8080
When working in an EE worktree (detected by: the working directory is under a worktrees/ directory):
dev-tools/setup-worktree.sh ../worktrees/fooThis copies the gitignored cli/src/main/resources/application-*.yml files from the main checkout into the worktree. Without this step Kestra cannot boot in the worktree. The script is idempotent — safe to re-run.
- Use tenant isolation for multi-tenant features
- Implement proper authorization with
@HasAnyPermission - Handle secrets securely (never log sensitive data)
- Implement pagination for large datasets
- Use streaming for large file operations
- Cache frequently accessed data appropriately
- Initialize collections with the expected size to avoid resizing overhead
Common Issues:
- Build failures: Run
./gradlew cleanand retry - Test failures: Check for service dependencies (Docker containers)
- Frontend issues: Ensure Node.js version matches package.json requirements
Debugging:
- Use IDE debugging with remote JVM debugging
- Use Micronaut's built-in health endpoints
- Enable debug logging:
--logging.level.io.kestra=DEBUG - Use JUnit and Vitest reports for test failures
Core Modules:
cli- Command Line Interfacecore- Core functionalitywebserver- Web serverui- Vue 3 frontend applicationexecutor- The component responsible for managing execution statescheduler- The component responsible for scheduling polling and schedule triggersworker- The component that executes tasks and manages worker instancesworker-controller- The component that manages worker instances and job distributionindexer- The component responsible for indexing executionsplateform- provides the Platform Bill of Materials (BOM) for dependency management
Queuing Layer:
queue- Core API for queue implementationsqueue-jdbc- JDBC-based queue implementation
Data Layer:
jdbc-*- Database implementations (H2, Postgres, MySQL)
Testing Modules:
tests- Common test utilities and base classesjmh-benchmark- JMH benchmarks for performance testing
Key Patterns:
- Repository pattern for data access
- Service layer for business logic
- Controller layer for HTTP endpoints
- Builder pattern for object construction (often with Lombok
@Builder)
- Always add tests, keep your branch rebased instead of merged, and adhere to the commit message recommendations from https://www.conventionalcommits.org/en/v1.0.0.
- Use types: chore, feat, fix, refactor, test, docs, build
- Use scopes: apps, assets, core, dashboards, deps, design-system, executions, flows, iam, namespaces, plugins, secrets, storage, scheduler, system, tasks, tenants, tests, topology, triggers, variables, version, worker
- Classify an issue with its GitHub issue type, not a
kind/*label. Thekind/buglabel is retired — do not add it. Set the type instead:gh issue create --title …followed bygh issue edit <number> --type Bug, orgh issue edit <number> --type Task|Feature|Epic. Available types areTask,Bug,FeatureandEpic(list them withgh api /orgs/kestra-io/issue-types). - Do add the
area/*labels —area/frontend,area/backend,area/devops,area/docs,area/plugin,area/qa,area/analytics— since those drive routing and are still in use. - Leave triage labels such as
kind/cooldowntokestrabot; it applies them automatically on new issues.
This document should be updated as the codebase evolves. When in doubt, follow existing patterns in the codebase and maintain consistency with established conventions.
MANDATORY — never hardcode user-facing strings. Every label, button, tooltip, placeholder, dialog/section title, table-column header, and toast/confirm message rendered to the user MUST go through vue-i18n: t("key") (or :label/:tooltip bindings) in components, and <i18n-t keypath="..."> with named slots when the string embeds markup or a component (e.g. a <code> fragment). Never write a literal user-facing string in a template, a :tooltip/:label attribute, or a toast.* call. Reuse existing generic keys (cancel, delete, edit, save, add, id, description, namespace, revision, …) instead of duplicating them; put feature-specific strings under one namespaced object (e.g. "reusableInputs": { … }). After adding keys to en.json, propagate them to every language (translation generation script) so the missing-keys check stays clean — a key present only in en.json fails the check.
Translation files live in ui/src/translations/. There is one JSON file per language code (e.g. de.json, fr.json) plus the source en.json.
Run the check script from the ui/ directory:
cd ui && npm run translations:checkA clean run reports No missing keys., No extra keys. and No stale keys. for every language. Anything listed must be fixed before merging — the same check runs as a PR gate.
Enterprise Edition: EE-only keys live in
ui-ee/src/translations/ee_translations/en.jsonand are checked separately — runnpm run translations:checkinui-eeas well (seekestra-ee/AGENTS.md→ "Frontend i18n").
Changing an existing English value is a translation change. Every key carries a fingerprint of the English text its translations were generated from, so editing en.json — even just the capitalisation — marks that key stale in all twelve languages and fails translations:check until it is regenerated. Run npm run translations:generate and commit the result alongside your change.
This is deliberate: before it existed, edited values were never propagated, and a rename of "SuperAdmin" to "Superadmin" sat un-translated in eleven locales for a year (#10656).
Prefer npm run translations:generate (needs GEMINI_API_KEY); it fills missing keys and re-translates stale ones on its own, with no flag to remember. Pass true to force a full re-translation of everything.
If you must write a translation by hand:
- Identify gaps by running
npm run translations:check. - Follow these translation rules (mirroring
ui/scripts/translations/generateTranslations.ts, the generator shared by OSS and EE):- Reserved English terms — never translate:
kv store,namespace,tenant,flow,subflow,task,log,blueprint,id,trigger,label,key,value,input,output,port,worker,backfill,healthcheck,min,max. - ALL-CAPS status labels stay in English:
WARNING,FAILED,SUCCESS,PAUSED,RUNNING, etc. - Preserve
{placeholder}variables exactly — vue-i18n uses a single pair of braces. Do not translate the name inside the braces, do not rename it, and never write{{placeholder}}: double braces are a compile error (Not allowed nest placeholder) and maket()throw at render time. Each translation must carry exactly the same placeholders as the English source — no invented ones, none dropped. - Use natural UI terminology — avoid false friends or overly literal translations (e.g. German: Execution → Ausführung, Theme → Modus, State → Zustand).
- Reserved English terms — never translate:
- Insert the translated keys into the correct position in the target language JSON, mirroring the key order of
en.json. - Re-run
npm run translations:checkto confirm everything is clean before committing.
The tooling itself lives in ui/scripts/translations/ and is shared with EE, which keeps only thin entry points. Rules live in .mjs so the dependency-free PR gate can apply them; file IO and orchestration stay in .ts.
Two branches that both touch en.json will both regenerate ui/scripts/translations/fingerprints.json, so it conflicts often. Never hand-merge the hashes and never pick a side — a hash says "this English text is what the twelve translations were generated from", so choosing the wrong one silently marks a drifted key as current and the drift becomes invisible again.
Resolve it the same way as a kestra-sdk conflict — regenerate:
git checkout --ours ui/src/translations/*.json ui/scripts/translations/fingerprints*.json
cd ui && npm run translations:generate # fills whatever the other branch added
npm run translations:check # must report no missing / extra / stale keysen.json itself normally merges cleanly, since branches usually add different keys; it is the generated files that collide.