Define a task

Author a custom task definition to use in Lattice

This guide describes best practices for authoring custom task definitions for use in Lattice. Custom task definitions allow you to extend Lattice’s tasking capabilities with your own specialized workflows.

Before you begin

Authoring tasks

A custom task is a Protobuf message type that contains the information your asset requires to execute a task autonomously. The complexity of your task definition depends on your integration’s requirements. For example:

  • Simple tasks: A sensor may define an on/off toggle message
  • Entity-targeted tasks: A tracking system may define a message containing an entity_id to track
  • Complex tasks: An autonomous vehicle may define multiple parameters including waypoints, speed limits, sensor configurations, and priority levels

Custom tasks give you the flexibility to model domain-specific workflows while maintaining type safety and compatibility across your Lattice deployment.

At its simplest, a task definition is a single Protobuf message. For example, the following schema defines a task that turns a sensor on:

sensor.proto
1syntax = "proto3";
2
3package org.example.sensor.v1;
4
5// Turn the sensor on.
6message SensorOn {}

Next, you’ll learn how to organize definitions into packages, how Lattice identifies them, and how to model richer task parameters.

Create a package

Define your tasks as Protobuf messages as part of a package. A package should contain all relevant message types to compose tasks that express an integration’s capabilities. Naming your package clearly and precisely is crucial to avoid potential naming conflicts.

Use a consistent package naming pattern: org.repository.package.version

Where:

  • org: Your organization name in the Schema Registry.
  • repository: Your repository name in the Schema Registry.
  • package: A descriptive name for your task domain, for example, reconnaissance, navigation, or sensor.
  • version: The API version, for example, v1, v2 v1alpha or v2test.
1// Example
2package company.isr.imaging.v1test;

During early development, mark your package as unstable by appending a stability marker (alpha, beta, or test) to the version.

1package org.example.reconnaissance.v1beta;
2package org.example.navigation.v2beta;

The Schema Registry enforces breaking change detection on all packages, regardless of the stability marker. If you need to make a breaking change, you must create a new version.

Fully qualified names

The fully qualified name of a message consists of its package and message name:

1package org.example.reconnaissance.v1beta;
2
3message Objective {
4 // ...
5}

The above message yields the name: org.example.reconnaissance.v1beta.Objective.

All protobuf type URLs are prefixed with type.googleapis.com. You will always reference your task definition by its type URL, for example: type.googleapis.com/org.example.reconnaissance.v1beta.Objective.

Globally unique names

Each fully qualified message name must be globally unique within the Schema Registry. This prevents naming conflicts when multiple organizations publish schemas.

This type URL is also how you advertise a task on an asset. The Entity model’s taskCatalog lists the tasks an asset can perform, and each entry’s taskSpecificationUrl is the type URL of a task definition:

taskCatalog
1"taskCatalog": {
2 "taskDefinitions": [
3 {
4 "taskSpecificationUrl": "type.googleapis.com/org.example.reconnaissance.v1beta.Objective"
5 }
6 ]
7}

An operator can only assign a task to an asset if the asset’s taskCatalog advertises that task’s type URL. To learn how to publish an asset with a taskCatalog and process the tasks it receives, see Integrate an agent.

For more information about Protobuf packages and naming, see Protobuf files and packages in the Buf documentation.

Breaking change protection

The Schema Registry enforces breaking change detection for all packages. This ensures backward compatibility for consumers of your schemas. You can deprecate old fields, or if necessary, release a new version of your package.

Breaking changes (prevented)Non-breaking changes (allowed)
Removing or renaming fieldsAdding new fields
Changing field typesAdding new enumerated values
Changing field numbersAdding new messages
Removing enumerated valuesDeprecating fields (using the deprecated option)

For a complete guide on breaking changes, see the Buf breaking change detector documentation.

Define a custom schema

Every task in Lattice is represented by the Task message. Its specification field is a google.protobuf.Any, which lets Lattice carry any custom task type without knowing its schema in advance:

1message Task {
2 // Version of this task.
3 TaskVersion version = 1;
4
5 // The path for the Protobuf task definition, and the complete task data.
6 // Your custom task message is packed into this Any field.
7 google.protobuf.Any specification = 3;
8
9}

Lattice packs the message you define into this specification field when a task is created. To author an example custom task, create a new .proto file. For example:

objective.proto
1syntax = "proto3";
2
3package org.example.reconnaissance.v1beta;
4
5// LLA is a geodetic position (latitude, longitude, altitude).
6message LLA {
7 // Latitude in degrees.
8 double latitude_degrees = 1;
9
10 // Longitude in degrees.
11 double longitude_degrees = 2;
12
13 // Altitude in Height Above Ellipsoid (WGS84) in meters.
14 double altitude_hae_m = 3;
15}
16
17// Objective represents the target of a reconnaissance task. The target is
18// either an entity to reconnoiter or a fixed geodetic point.
19message Objective {
20 oneof target {
21 // Entity to reconnoiter, identified by its entity ID.
22 string entity_id = 1;
23
24 // Fixed geodetic point to reconnoiter.
25 LLA lla = 2;
26 }
27}
Best practices
  • Use descriptive comments: Document each field and enum value to help other developers understand your schema.
  • Use wrapper types: For optional numeric fields, use google.protobuf.*Value wrapper types instead of primitive types.
  • Start with stability markers: Use v1alpha or v1beta during development to allow breaking changes.
  • Reserved field 0: Always reserve enum value 0 for an INVALID or UNSPECIFIED variant. In proto3, an unset enum field defaults to 0, so reserving it keeps an intentional value from being indistinguishable from an unset one.

Common patterns

sensor.proto
1syntax = "proto3";
2
3package company.sensor.v1;
4
5// Use the entity_id field task your Asset against an Entity
6message TrackEntity {
7 string entity_id = 1;
8}
flight.proto
1syntax = "proto3";
2
3package company.uas.v1;
4
5// Use Message types to compose a more complex Task
6message Coordinate {
7 double lat = 1;
8 double lon = 2;
9}
10
11// A target could be either a lat/lon, or an Entity that we can track
12message Target {
13 oneof target {
14 string entity_id = 1;
15 Coordinate coordinate = 2;
16 }
17}
18
19message FlightConstraints {
20 float min_altitude_agl_m = 1;
21 float max_altitude_agl_m = 2;
22 float max_ias_kts = 3;
23 float min_ias_kts = 4;
24}
25
26enum FlightPattern {
27 FLIGHT_PATTERN_GRID = 1;
28 FLIGHT_PATTERN_ORBIT = 2;
29 FLIGHT_PATTERN_SECTOR_SEARCH = 3;
30}
31
32// Task Message
33message Survey {
34 Target target = 1;
35 FlightPattern flight_pattern = 2;
36 FlightConstraints flight_constraints = 3;
37}

What’s next?