Docs
  • Solver
  • Models
    • Field Service Routing
    • Employee Shift Scheduling
    • Pick-up and Delivery Routing
  • Platform
Try models
  • Timefold Solver SNAPSHOT
  • Using Timefold Solver
  • Building a service (Preview)
  • REST API
  • Edit this Page

Timefold Solver SNAPSHOT

    • Introduction
    • PlanningAI concepts
    • Getting started
      • Overview
      • Hello World Quick Start Guide
      • Quarkus Quick Start Guide
      • Spring Boot Quick Start Guide
      • Vehicle Routing Quick Start Guide
    • Using Timefold Solver
      • Using Timefold Solver: Overview
      • Configuring Timefold Solver
      • Modeling planning problems
      • Running Timefold Solver
      • Benchmarking and tweaking
      • Building a service (Preview)
        • REST API
        • Service Model and Enricher
        • Constraint weights (optional)
        • Demo data (optional)
        • Exposing metrics (optional)
    • Constraints and score
      • Constraints and Score: Overview
      • Score calculation
      • Understanding the score
      • Adjusting constraints at runtime
      • Load balancing and fairness
      • Performance tips and tricks
    • Optimization algorithms
      • Optimization Algorithms: Overview
      • Construction heuristics
      • Local search
      • Exhaustive search
      • Neighborhoods: A new way to define custom moves
      • Move Selector reference
    • Responding to change
    • Integration
    • Design patterns
    • FAQ
    • New and noteworthy
    • Upgrading Timefold Solver
      • Upgrading Timefold Solver: Overview
      • Upgrade from Timefold Solver 1.x to 2.x
      • Upgrading from OptaPlanner
      • Backwards compatibility
      • Migration Guides
        • Variable Listeners to Custom Shadow Variables
        • Chained planning variable to planning list variable
    • Plus/Enterprise Editions
      • Installation
      • Performance improvements
      • Score analysis
      • Recommendation API
      • Nearby selection
      • Multithreaded solving
      • Partitioned search
      • Constraint profiling
      • Multistage moves
      • Throttling best solution events

REST API

The REST API module will automatically generate REST API endpoints so you can interact with the optimization service.

This page describes features which are only relevant when running Timefold Solver as a Service (Preview). The information on these pages may describe functionality which may be changed or even removed in a future release.

1. Setup

In order for the API to be automatically generated, Timefold Solver will look for implementations of a couple interfaces. The following are expected.

  • An implementation of ModelInput to define the class which will be mapped from JSON to Java on API requests.

  • An implementation of ModelOutput to define the class which will be mapped from Java to JSON on API responses.

  • An interface which extends ModelRest to define the root path of the REST API.

For example, in the case of School Timetabling, those classes could look as follows.

// TimetableDto.java
public class TimetableDto implements ModelInput, ModelOutput {

    @Schema(required = true, description = "The unique identifier of timetable")
    private String name;
    @Schema(required = true, description = "List of timeslots")
    private List<Timeslot> timeslots;
    @Schema(required = true, description = "List of lessons")
    private List<Lesson> lessons;

    // Getters, Setters and constructor excluded.
}

// TimetableSchedulingResource.java
@Tag(name = "School Timetabling",
        description = "School timetabling service assigning lessons to timeslots.") // OpenAPI documentation annotation
@Path("/v1/timetables") //sets the root path
public interface TimetableSchedulingResource extends ModelRest {
}

Note how the ModelInput and ModelOutput interface can be placed on the same class.

Since an OpenAPI specification will also be automatically generated, it’s important to properly document your ModelInput and ModelOutput classes with OpenAPI annotations.

2. Generated endpoints

The following endpoints are generated by the REST module. The <root> is determined by the @Path annotation on the ModelRest interface (/v1/timetables in the example above).

  • GET /<root>: Retrieve all registered optimization datasets

  • POST /<root>: Create a new optimization dataset

  • GET /<root>/{id}: Retrieve the (intermediate) result of a dataset optimization run

  • GET /<root>/{id}/score-analysis: Retrieve score analysis of an optimization dataset

  • DELETE /<root>/{id}: Terminate a dataset optimization run

If your model provides demo data, the following endpoints are also created.

  • GET /<root>/demo-data: Retrieve all available demo dataset names.

  • GET /<root>/demo-data/{name}: Retrieve the demo dataset with the given identifier

3. Request and response structure

The generated endpoints do not accept/return the pure ModelInput/ModelOutput objects as JSON. Instead, a fixed envelop is used for both the requests and responses of solver actions.

3.1. Structured solving request

A Solving Request always contains at least 2 major parts, the modelInput object and a config object.

  • The modelInput object contains the JSON formatted ModelInput object defined in Java.

  • The config object has two fields: the run and the model.

Additionally, more fields might be added for specific model implementations.

Example Request
{
    "config": {
      "run": {
        "name": "run name",
        "termination": {
          "spentLimit": "PT5M",
          "unimprovedSpentLimit": "PT10S"
        },
        "maxThreadCount": 1,
        "tags": []
      },
      "model": {
        "overrides": "<model-specific configurations>"
      }
    },
    "modelInput" : "<ModelInput class as JSON>"
}

The run configuration has four fields:

  • name - The run name (if empty, it will be generated).

  • maxThreadCount - The maximum thread count, which indicates the maximum number of threads to be used for solving. If not provided, 1 will be used.

  • tags - The tags, which are a set of optional tags to be assigned to the run.

  • termination - The termination properties determining how long the solver should run:

    • spentLimit - The maximum duration to keep the solver running (ISO 8601 Duration). If omitted, no maximum duration will be set (platform limits will apply).

    • unimprovedSpentLimit - The maximum unimproved score duration, i.e. if the score has not improved during this period, the solver will terminate (ISO 8601 Duration). If omitted, the diminished returns termination will be used.

The diminished returns termination is the recommended default setting. This termination is desirable since it terminates based on the relative rate of improvement, and behaves similarly on different hardware and different problem instances. unimprovedSpentLimit should be set only when necessary.

The model configuration is a model-specific field, that contains additional global model configuration attributes. See Model Configuration Overrides for more information.

3.2. Structured solving response

A Solving Response always contains at least 2 major elements, the modelOutput object and a metadata object.

  • The modelOutput object contains the JSON formatted ModelOutput object defined in Java.

  • The metadata object provides more details about the optimization run.

Additionally, more fields might be added for specific model implementations:

  • inputMetrics: metrics about the input of the planning problem see: ./exposing-metrics.adoc#modelInputMetrics

  • kpis: metrics about the output of the planning problem see: ./exposing-metrics.adoc#modelOutputMetrics

Example Response
{
  "metadata": {
    "id": "dataset-id",
    "name": "dataset-name",
    "submitDateTime": "2022-03-10T12:15:50-04:00",
    "startDateTime": "2022-03-10T12:15:50-04:00",
    "activeDateTime": "2022-03-10T12:15:50-04:00",
    "completeDateTime": "2022-03-10T12:15:50-04:00",
    "shutdownDateTime": "2022-03-10T12:15:50-04:00",
    "solverStatus": "NOT_SOLVING",
    "score": "string",
    "tags": [],
    "validationResult": {
      "summary": "VALIDATION_NOT_SUPPORTED",
      "errors": [],
      "warnings": []
    }
  },
  "modelOutput": <ModelOutput class as JSON>,
  "inputMetrics": {
    "lessons": 200,
    "timeslots": 50
  },
  "kpis": {
    "unassignedLessons": 0,
    "maxConsecutiveLessons": 3,
    "earliestTimeslotStart": "2027-02-02T09:00:00Z",
    "latestTimeslotStart": "2027-02-05T17:00:00Z"
  }
}

The metadata object contains a couple of fields:

  • id - The dataset id, as generated by the Solver.

  • name - The dataset name.

  • solverStatus - The status of the optimization.

  • score - The score of the solution.

  • tags - The assigned tags of the dataset.

  • validationResult - The result of the validation. This might signal errors in the original input.

The remaining fields are timestamps for each step in the optimization lifecycle.

The metadata object contains five key timestamps, each marking a distinct phase in the process:

  • submitDateTime: The moment the dataset is submitted. At this stage, the dataset is queued and has not yet begun solving. The dataset status is SOLVING_SCHEDULED.

  • startDateTime: The moment the run begins initializing. During this phase, the dataset is not solving yet, but the model is being built and enriched with external data (e.g., incorporating map information for models involving geographical locations). The dataset status is SOLVING_STARTED.

  • activeDateTime: The moment the solving phase begins. At this stage, the actual problem-solving process starts. The dataset status is SOLVING_ACTIVE.

  • completeDateTime: The moment the solving phase concludes. At this point, the dataset has finished solving but has not yet undergone any post-processing (e.g., score analysis or waypoint enrichment for geographical models). The dataset status is either SOLVING_COMPLETED or SOLVING_FAILED.

  • shutdownDateTime: The moment the post-processing phase finishes. All tasks, including post-processing, are completed, and the dataset optimization is fully finalized.

4. Decoupling API interaction from the solver domain

While it is possible to add the ModelInput and ModelOutput directly on the SolverModel (the @PlanningSolution class used by the solver), it’s often beneficial to decouple the classes used for the REST API interactions and the classes used for the optimization.

To convert between ModelInput / ModelOutput and the SolverModel, a ModelConvertor implementation can be provided.

@ApplicationScoped
public class TimetableConvertor implements ModelConvertor<HardSoftLongScore, TimetableDto, Timetable, EmptyModelConfigOverrides, EmptyModelInputMetrics, EmptyModelOutputMetrics, Timetable> {

    @Override
    public Timetable toSolverModel(TimetableDto modelInput, TimetableDto previousModelOutput, ModelConfig<EmptyModelConfigOverrides> modelConfig) {
        return // Mapping logic
    }

    @Override
    public Timetable toSolverModel(TimetableDto modelInput, ModelConfig<EmptyModelConfigOverrides> modelConfig) {
        return // Mapping logic
    }

    @Override
    public TimetableDto toModelOutput(Timetable solverModel) {
        return // Mapping logic
    }
}

This mechanism should only be used for ModelInput → SolverModel → ModelOutput mapping. For enhancing the SolverModel, use the SolverModel Enrichment mechanism instead.

5. Validating REST input

By default, validation on the input is based on the OpenAPI Specification annotations. Additional validations can be added by implementing the ModelValidator interface.

@ApplicationScoped
public class TimetableValidator implements ModelValidator<TimetableDto, TimetableConfigOverrides> {

    @Override
    public void validate(ValidationBuilder validationBuilder, TimetableDto input, ModelConfig<TimetableConfigOverrides> modelConfig) {

        //validation logic here, simplified example here.
        if(hasDuplicateTeacherNames(input)) {
            validationBuilder.addIssue(TimetableValidationIssue.DUPLICATE_TEACHER.asIssueType(), new DuplicateTeacherDetail("Ann"));
        }
    }
}

public enum TimetableValidationIssue {

    DUPLICATE_TEACHER(IssueCode.of("DUPLICATE_TEACHER"), IssueSeverity.ERROR,
            "Duplicate teacher names found.");

    private final IssueType issueType;

    TimetableValidationIssue(IssueCode code, IssueSeverity severity, String message) {
        this.issueType = new IssueType(code, severity, message);
    }

    public IssueType asIssueType() {
        return issueType;
    }

    public record DuplicateTeacherDetail(String teacherName) implements IssueDetail {
    }
}

The ValidationBuilder supports issue types of different severity:

  • IssueSeverity.ERROR: error level: processing can not continue. Results in a BAD_REQUEST (400) HTTP error.

  • IssueSeverity.WARNING: warning level: processing will continue.

All validation information can be accessed either in a simple textual form, or as structured machine-readable objects.

5.1. Simple validation result in metadata object

The metadata object is a part of the model response json and contains the validation result in a textual form to provide an immediate feedback on the input json.

Validation result in response json.
{
  "metadata": {
    "id": "dataset-id",
    "name": "dataset-name",
    "submitDateTime": "2022-03-10T12:15:50-04:00",
    "startDateTime": "2022-03-10T12:15:50-04:00",
    "activeDateTime": "2022-03-10T12:15:50-04:00",
    "completeDateTime": "2022-03-10T12:15:50-04:00",
    "shutdownDateTime": "2022-03-10T12:15:50-04:00",
    "solverStatus": "NOT_SOLVING",
    "score": "string",
    "tags": [],
    "validationResult": {
      "summary": "ERRORS",
      "errors": ["Duplicate teacher names found."],
      "warnings": []
    }
  },
  "modelOutput": "<ModelOutput class as JSON>"
}

5.2. Machine-readable validation result

Additionally, a richer validation result can be obtained by a dedicated REST endpoint:

GET /v1/timetables/{id}/validation-result

Assuming the example TimetableValidator, the endpoint provides the following response:

Validation result available via GET /v1/timetables/{id}/validation-result.
{
  "status" : "ERRORS",
  "issues" : [ {
    "id" : 1,
    "code" : "DUPLICATE_TEACHER",
    "severity" : "ERROR",
    "detail" : {
      "teacherName" : "Ann"
    }
  } ]
}

The machine-readable validation result enables for corrective actions. In this example, the timetable can be corrected by removing the duplicate teacher Ann.

6. Adding custom endpoints

You can add custom endpoints in the ModelRest implementation. For example, if you want to extend the Timetable REST API, you can add custom Jakarta REST endpoints. These endpoints should also be documented appropriately for the automatically generated OpenAPI specification.

@Tag(name = "School Timetabling",
        description = "School timetabling service assigning lessons to timeslots.") //OpenAPI documentation annotation
@Path("/v1/timetables") //sets the root path
public interface TimetableSchedulingResource extends ModelRest {

    @APIResponses(value = {
        @APIResponse(responseCode = "500", description = "In case of processing errors",
                content = @Content(mediaType = MediaType.APPLICATION_JSON,
                        schema = @Schema(implementation = ErrorInfo.class))),
        @APIResponse(responseCode = "200", description = "List of all teachers in all optimization runs.",
                content = @Content(mediaType = MediaType.APPLICATION_JSON,
                        schema = @Schema(implementation = ListTeachersResponseDto.class))) })
    @Operation(operationId = "list-teachers-in-solver-model",
            summary = "Lists all teachers in the solver model.")
    @GET
    @Path("/insights/teachers")
    @Produces(MediaType.APPLICATION_JSON)
    default Response insightsTeachers() {
        var teachers = //get the list of teachers
        return Response.ok(teachers).build();
    }
}

7. OpenAPI specification

The OpenAPI specification is a machine-readable document that defines the structure, endpoints, request/response formats, and authentication of a RESTful API. A specification file is automatically generated from the MicroProfile OpenAPI annotations on the Java classes.

For more info on which annotations are available and how they relate to the generated OpenAPI specification, check out the official MicroProfile OpenAPI specification here.

After building the model with mvn verify, the generated API spec can be found in the target folder: target/openapi-schema/openapi.json.

7.1. Deliberate API changes

As with any API, breaking the REST API in newer versions of your model should be avoided to ensure backward compatibility and prevent disruptions for existing consumers. Because Timefold Solver takes care of a lot of boilerplate code, it might be harder to see when an API change was accidentally introduced.

Timefold Solver supports comparing a pre-existing OpenAPI specification file in your project src/build/openapi.json with the one generated during the build. If there is any difference between the 2 files, the build will fail.

If the API changes are intentional, run the following command which will copy the generated target/openapi-schema/openapi.json to src/build/openapi.json file.

mvn clean package -Dupdate-api

This approach ensures that any change to the API has to be reflected in the src/build/openapi.json file.

We recommend to keep this file in Version Control, this makes sure the changes made are deliberate (committed and pushed) rather than accidental.

7.2. Customize summary of solver methods

As mentioned before, the REST endpoints for solver methods are automatically generated. This means users do not have the option to set the OpenAPI Annotations for those methods.

Timefold Solver does support setting a custom summary through the use of properties in the application.properties file. This uses the following format:

timefold.rest.<operationname>.summary=Your custom summary here

The <operationname> is the name of the method in the API Spec (operationId field).

For example, if you want to override the OpenAPI summary for the schedule operation, add the following entry in your properties file:

timefold.rest.schedule.summary=Request a timetable to be solved asynchronously.
  • © 2026 Timefold BV
  • Timefold.ai
  • Documentation
  • Changelog
  • Send feedback
  • Privacy
  • Legal
    • Light mode
    • Dark mode
    • System default