Creating a Scheduling Goal with External Events
Now that we have a plan with a Derivation Group associated to it, we can create & run procedural scheduling goals that make use of External Events.
This tutorial page will talk specifically about using External Events in procedural scheduling. A more in-depth guide to procedural scheduling can be found here.
The following sections is a partial walk-through of creating a procedural goal with some of the general goal setup removed. For reference, the full goal as it should be written is included in the Full Example Goal section.
Creating a Scheduling Goal
As a pre-requisite to creating a scheduling goal, follow the Create a project from the template
steps found in the procedural scheduling documentation.
Example: Scheduling an activity for SampleTypeA
One simple use case for External Events in scheduling goals is adding an activity instance whenever an External Event occurs. In this example we'll create a goal that creates a new activity for each occurrence of a SampleTypeA
External Event in the plan.
With the project initialized, create a new file within scheduling/src/main/java/scheduling/procedures/
called DemoSchedulingGoal.java
.
External Events can be collected within the scheduling goal by creating EventQuery
s, and using plan.events(yourQueryHere).collect()
. To grab all the External Events of type SampleTypeA
, we can write the following EventQuery
:
EventQuery sampleTypeAEvents = new EventQuery(null, List.of("Sample Type A"), null);
The above EventQuery
only includes a value for the middle argument, eventTypes
. Queries can also filter by derivation group using the first argument of the EventQuery
, or by source using the last argument.
To retrieve the External Events associated with this EventQuery
, we can use the following line:
var exampleEvents = plan.events(sampleTypeAEvents).collect();
Prior to creating our new activity instances, we can simulate the plan and take note of all the existing activity instances:
final var simResults = plan.simulate();
final var existingSpans = simResults.instances();
Next, we can iterate over all of our collected events and create a new activity for each one if there isn't a SampleTypeA
activity there already:
for (var currentEvent : exampleEvents) {
if (!areThereSpansForType(existingSpans, currentEvent.getInterval().start, newActivityType)) {
final var newActivityName = currentEvent.key + " Activity";
final var currentEventExampleAttribute = currentEvent.attributes.get("EventExampleAttribute").asInt().get();
plan.create(
new NewDirective(
new AnyDirective(Map.of(
"temperature", SerializedValue.of(123.1),
"tbSugar", SerializedValue.of(currentEventExampleAttribute),
"glutenFree", SerializedValue.of(false)
)),
newActivityName,
newActivityType,
new DirectiveStart.Absolute(currentEvent.getInterval().start)
)
);
}
}
And finally, add a plan.commit();
after the loop to apply the changes.
Compile and upload this goal and associate it with the plan we previously created. After running scheduling, a BakeBananaBread
activity will be scheduled at the start of each SampleTypeA
External Event. See Uploading the Scheduling Goal for instructions.
Example: Scheduling an activity for SampleTypeA
using attributes
Building on the above example, you may want to constrain your goal to only plan activity instances on the SampleTypeA
External Events that have their EventExampleAttribute
attribute set to 1
. The previous example gathers the EventExampleAttribute
for use with the tbSugar
argument on the BakeBananaBread
activity but instead we could use it in the for
loop to filter the External Events we're using for planning:
for (var currentEvent : exampleEvents) {
if (!areThereSpansForType(existingSpans, currentEvent.getInterval().start, newActivityType)) {
final var currentEventExampleAttribute = currentEvent.attributes.get("EventExampleAttribute").asInt().get();
if (currentEventExampleAttribute == 1) {
final var newActivityName = currentEvent.key + " Activity";
plan.create(
new NewDirective(
new AnyDirective(Map.of(
"temperature", SerializedValue.of(123.1),
"tbSugar", SerializedValue.of(currentEventExampleAttribute),
"glutenFree", SerializedValue.of(false)
)),
newActivityName,
newActivityType,
new DirectiveStart.Absolute(currentEvent.getInterval().start)
)
);
}
}
}
Full Example Goal
package scheduling.procedures;
import gov.nasa.ammos.aerie.procedural.scheduling.Goal;
import gov.nasa.ammos.aerie.procedural.scheduling.annotations.SchedulingProcedure;
import gov.nasa.ammos.aerie.procedural.scheduling.plan.EditablePlan;
import gov.nasa.ammos.aerie.procedural.scheduling.plan.NewDirective;
import gov.nasa.ammos.aerie.procedural.timeline.collections.Instances;
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyDirective;
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.AnyInstance;
import gov.nasa.ammos.aerie.procedural.timeline.payloads.activities.DirectiveStart;
import gov.nasa.ammos.aerie.procedural.timeline.plan.EventQuery;
import gov.nasa.jpl.aerie.merlin.protocol.types.Duration;
import gov.nasa.jpl.aerie.merlin.protocol.types.SerializedValue;
import java.util.List;
import java.util.Map;
@SchedulingProcedure
public record DemoSchedulingGoal() implements Goal {
boolean areThereSpansForType(Instances<AnyInstance> spans, Duration startTime, String activityType) {
final var filteredByType = spans.filter(false, it -> it.getType().equals(activityType));
final var filteredByTime = filteredByType.filter(false, it -> it.getStartTime().equals(startTime));
return !filteredByTime.collect().isEmpty();
}
@Override
public void run(EditablePlan plan) {
final var newActivityType = "BakeBananaBread"; // This can be any activity type in your model!
EventQuery sampleTypeAEvents = new EventQuery(null, List.of("SampleTypeA"), null);
var exampleEvents = plan.events(sampleTypeAEvents).collect();
final var simResults = plan.simulate();
final var existingSpans = simResults.instances();
for (var currentEvent : exampleEvents) {
if (!areThereSpansForType(existingSpans, currentEvent.getInterval().start, newActivityType)) {
var currentEventExampleAttribute = currentEvent.attributes.get("EventExampleAttribute").asInt().get();
if (currentEventExampleAttribute == 1) {
final var newActivityName = currentEvent.key + " Activity";
plan.create(
new NewDirective(
new AnyDirective(Map.of(
"temperature", SerializedValue.of(123.1),
"tbSugar", SerializedValue.of(currentEventExampleAttribute),
"glutenFree", SerializedValue.of(false)
)),
newActivityName,
newActivityType,
new DirectiveStart.Absolute(currentEvent.getInterval().start)
)
);
}
}
}
plan.commit();
}
}
Uploading the Scheduling Goal
After the goal has been created and saved, it must be compiled following the steps in Getting Started. The following commands should be run from the root of the project directory:
./gradlew :scheduling:compileJava
./gradlew :scheduling:buildAllProceduralSchedulingJars
Afterwards, a DemoSchedulingGoal.jar
should be created in $project/scheduling/build/libs/
.
In the Aerie UI, navigate back to the plan view for our previously created plan and select Scheduling Goals
from the top-left drop-down menu. From there, click Manage Goals
and then the New
button to be directed to creating/uploading a new scheduling goal.

In the creation form, enter a name (for example, DemoSchedulingGoal
), swap the EDSL/Jar File
button to Jar File
, and then click Choose File
and select the previously created .jar
file. Your screen should show the following:

Back in the plan view, the goal can now be associated and run

Additional Resources
More detail on the topics discussed in this tutorial can be found under External Events