Example Validation rules for Dynamic Scheduling

I am seeking to build out validation rules the Dynamic Scheduling widget. The interface specifications in the docs here are helpful, but I am curious if there are some example implementations that I can take a look and learn from.

I got AIP to generate the following example which is referenced in the docs but for which implementation is never shown:

import { ScheduleObject, ScheduleObjectPrimaryKey } from "@foundry/ontology-api";
import { IFoORule, IRuleResult, IRuleResultDetails } from "./types";

const noOverlapRule: IFoORule = (scheduleObjectPrimaryKeys: ScheduleObjectPrimaryKey[]) => {
  const ruleResults: IRuleResult[] = [];

  // Get the schedule objects for the given primary keys
  const scheduleObjects = ScheduleObject.getByIds(scheduleObjectPrimaryKeys);

  // Check for overlapping time ranges
  for (const scheduleObject of scheduleObjects) {
    const overlaps = scheduleObjects.filter(
      (other) =>
        other.id !== scheduleObject.id &&
        other.startDate <= scheduleObject.endDate &&
        other.endDate >= scheduleObject.startDate
    );

    // If any overlaps are found, add a rule result for the schedule object
    if (overlaps.length > 0) {
      ruleResults.push({
        result: false,
        scheduleObjectPrimaryKey: scheduleObject.id,
        details: [
          {
            description: "This schedule object overlaps with another one.",
            relatedPuckIds: overlaps.map((o) => o.id),
          },
        ],
      });
    } else {
      ruleResults.push({
        result: true,
        scheduleObjectPrimaryKey: scheduleObject.id,
      });
    }
  }

  return ruleResults;
};

Nice simple function which follows pretty simply from the interface declaration. Still some open questions on my end, though, such as:

  • The AIP code above and the interface declarations use ScheduleObject and ScheduleObjectPrimaryKey – what are these? The import statement at the top of the AIP code fails compiler check.
    • If the ScheduleObject type is just a stand-in for the specific schedulable object type we are using, how can a single function support multiple schedulable object types? I notice the widget allows you to select from multiple distinct sets of objects to instantiate the set of pucks for scheduling.
    • If it is an abstract type or interface, why won’t the import work?
  • What is in the list of scheduleObjectPrimaryKeys passed to the function? Are these all the scheduleObjects which have been scheduled?
  • Is it possible to pass any additional parameters to the function? (Seems like potentially not.)

Hey,

I’m Molly, I’m lead on Dynamic Scheduling team - would love to help out! I will answer your questions in order. Let me know where these don’t make sense or where you’d like to dive deeper.

Responses are inline:

Note: Any code generated by AIP should be taken with a grain of salt. I’ve found for Typescript FoO, LLMs tend to make up methods that don’t exist and over generalize.

  • The AIP code above and the interface declarations use ScheduleObject and ScheduleObjectPrimaryKey – what are these? The import statement at the top of the AIP code fails compiler check.
    • If the ScheduleObject type is just a stand-in for the specific schedulable object type we are using, how can a single function support multiple schedulable object types? I notice the widget allows you to select from multiple distinct sets of objects to instantiate the set of pucks for scheduling.
    • If it is an abstract type or interface, why won’t the import work?

RESPONSE:
Schedule Object IS a stand in here that your LLM inputted. You should replace it with a reference to your schedule Object Type (API name) that this rule will interact with (examples to follow in next reply). A set of rules are registered per Object Type. This means that you can have multiple schedules (“pucks”) appear in the Scheduling Gantt chart from multiple Object Types. The widget looks at each puck and its corresponding Object Type, and the rules configured to THAT Object Type. Each time you load or make a change to the schedule, these rules will be re-evaluated by the widget across all given Object Types present.

The ability to generically reference the concept of a “schedule” object type is not available. So, if you have the same rule for multiple schedules, recreate this function for each Object Type. If your different Object Types have the same property API names, then you could in theory use the same function, because the property API references to endDate + startDate + id (in your example) will work across those Object types. The two main things to note if you do: you still need to populate the scheduleObject variable (the const in your example) from across these Object Types / their Object Type API names, and you would still need to register this rule on each Object Type in Ontology.

For the imports at the top:
1. import your Object Types into Code Repositories
2. Replace the generic reference the LLM generated with the relevant Object Type API names
3. You can now reference these Object Types across your Functions!

  • What is in the list of scheduleObjectPrimaryKeys passed to the function? Are these all the scheduleObjects which have been scheduled?

RESPONSE:
Yes, correct! The widget will pass in the pucks present in the viewable window that have either not yet had their rules run before, or their current rule state is invalid and needs to be refreshed (for ex, if you just moved that puck to a new location). You can filter to any failing vs passing rules in the “Filter” button at the top of the widget.

  • Is it possible to pass any additional parameters to the function? (Seems like potentially not.)

RESPONSE:
This is not currently a feature. You can of course reference additional non-schedule objects or other properties within your function. We often see people create an Object that they edit in their Workshop that contains “parameters” they want to use on Rule run; they then reference this Object in their Rule Function.

EXAMPLES

Production Order Schedule

The below function is a rule for the Production Order object type, which is the schedule or “puck” that appears on the Gantt Chart. In this Ontology, the Material and Line are two additional Object Types that are linked to the Production Order. The Production Order gets allocated/assigned to a Line in the workflow.

This rule is checking if the Material Type associated with the Production Order’s material is compatible with the Line’s certifications.

@Function()
public checkOrdersCompatibleWithLineMaterialTypes(scheduleObjectPrimaryKeys: string[]): Array<IRuleResult> {
    const ordersSpec = Objects.search().productionOrder().filter(o => o.orderNumber.exactMatch(...scheduleObjectPrimaryKeys));
    const relevantMaterials = ordersSpec.searchAroundMaterial().all();
    const orders = ordersSpec.all();

    const lines = Objects.search().productionLine().all();
    const lineByLineId = keyBy(lines, l => l.lineId);
    const materialByMaterialId = keyBy(relevantMaterials, m => m.materialNumber);

    const result: IRuleResult[] = [];
    
    orders.forEach(o => {
        const lineMaterialTypes = lineByLineId[o.productionLineId ?? ""]?.lineMaterialTypes;
        const materialType = materialByMaterialId[o.materialNumber ?? ""]?.materialType;
        result.push({ result: lineMaterialTypes && materialType ? lineMaterialTypes.includes(materialType) : undefined, scheduleObjectPrimaryKey: o.orderNumber });
    });
    
    return result;
}

This rule is for the same workflow with Production Order. It checks if the Line is over its max capacity, given its assigned Production Orders.

@Function()
    public checkOrdersWithinLineCapacity(scheduleObjectPrimaryKeys: string[]): Array<IRuleResult> {
        const passedOrders = Objects.search().productionOrder().filter(o => o.orderNumber.exactMatch(...scheduleObjectPrimaryKeys)).all();
        const relevantLines = compact(uniq(passedOrders.map(po => po.productionLineId)));
        const relevantPlants = compact(uniq(passedOrders.map(po => po.plantId)));
        const orders = relevantLines.length > 0 ? 
            Objects.search()
                .productionOrder()
                .filter(o => o.productionLineId.exactMatch(...relevantLines))
                .filter(o => o.plantId.exactMatch(...relevantPlants))
                .all()
            : [];
        const ordersByLineId = mapValues(groupBy(orders, o => o.productionLineId), os => groupBy(os, o => o.scheduledStartDate?.valueOf()));

        const lines = Objects.search().productionLine().all();
        const lineByLineId = keyBy(lines, l => l.lineId);

        const result: IRuleResult[] = [];

        passedOrders.forEach(o => {
            const lineCapacity = lineByLineId[o.productionLineId ?? """"]?.lineCapacity;
            if (o.scheduledStartDate && lineCapacity) {
                const total = ordersByLineId[o.productionLineId ?? ""]?.[o.scheduledStartDate.valueOf()] ?? [];
                result.push({ result: total.length <= lineCapacity, scheduleObjectPrimaryKey: o.orderNumber })
            } else {
                result.push({ result: undefined, scheduleObjectPrimaryKey: o.orderNumber });
            }
        })

        return result;
    }

Nurse Shift Scheduling

This function is for a Nurse Shift Scheduling workflow. In this workflow, we have a NurseSchedule Object (aka the “shift”) that is assigned to a Nurse Object.

This rule checks if a Nurse if getting their preferred time of day.

@Function()
    public checkNurseShiftTimeOfDay(scheduleObjectPrimaryKeys: string[]): Array<IRuleResult> {
        const result: IRuleResult[] = [];

        const schedulesOs = Objects.search()._nurseSchedule().filter(s => s.scheduledShiftAssignmentId.exactMatch(...scheduleObjectPrimaryKeys));
        const nurses = schedulesOs.searchAround_nurse().all();
        const nursesById = keyBy(nurses, n => n.staffMember34Id);
        const schedules = schedulesOs.all();
        const schedulesById = keyBy(schedules, s => s.scheduledShiftAssignmentId);

        const shiftTimeOfDays = ["Day", "Night"];
        scheduleObjectPrimaryKeys.forEach(sId => {
            const schedule = schedulesById[sId];
            const nurse = schedule && schedule.staffMemberId ? nursesById[schedule.staffMemberId] : undefined;
            if (schedule && schedule.shiftStartTimestamp && nurse && shiftTimeOfDays.includes(nurse.shiftTimeOfDay ?? "")) {
                const startHour = schedule.shiftStartTimestamp.getHours();
                const isDayTime = startHour >= 6 && startHour < 18;
                result.push({ scheduleObjectPrimaryKey: sId, result: nurse.shiftTimeOfDay === "Day" ? isDayTime : !isDayTime });
            } else {
                result.push({ scheduleObjectPrimaryKey: sId, result: undefined });
            }
        });

        return result;
    }

Thanks Molly – super helpful. I’ll be playing around with this over the weekend and will revert with any follow-up questions.

Taylor

Add’l question: what is the proper way to apply the interface declarations in our index.js? Should we be importing somewhere or just copy/paste from the docs?

Hi Molly et all! The example functions were very helpful and I’ve managed to implement a function leveraging the same interface declarations and patterns. I’m starting with a simple “No Overlap” constraint (code below).

How exactly do we get the constraint to apply on the front-end widget?

As instructed in the docs, we have created a Build Planning Rule object which we believe has been correctly linked to the Build Planning Task “schedule object” using a M2M link with Type classes schedules:schedulable-rule-link/.

What else do we need to do?

Thanks,
Taylor

My code in case relevant:

export class BuildPlanningConstraintFunctions {
    
    @Function()
    public checkTaskOverlap(scheduleObjectPrimaryKeys: string[]): Array<IRuleResult> {
        const passedBuildTasks = Objects.search().buildPlanningTask().filter(t => t.buildPlanningTask.exactMatch(...scheduleObjectPrimaryKeys)).all();
        const relevantTechs = _.compact(_.uniq(passedBuildTasks.map(pbt => pbt.technicianPeopleSk)));
        const ruleResults: IRuleResult[] = [];

        const buildTasks = relevantTechs.length > 0 ?
            Objects.search()
                .buildPlanningTask()
                .filter(t => t.technicianPeopleSk.exactMatch(...relevantTechs))
                .all()
            : [];

        for (const taskObject of buildTasks) {
            const overlappingTasks = buildTasks.filter(
                (otherTask) =>
                    otherTask.buildPlanningTask != taskObject.buildPlanningTask &&
                    otherTask.technicianPeopleSk == taskObject.technicianPeopleSk &&
                    otherTask.buildPlanningTaskStartTime! <= taskObject.buildPlanningTaskEndTime! &&
                    otherTask.buildPlanningTaskEndTime! >= taskObject.buildPlanningTaskStartTime!       
            );

            if (overlappingTasks.length > 0) {
                ruleResults.push({
                    result: false,
                    scheduleObjectPrimaryKey: taskObject.buildPlanningTask,
                    details: [
                        {
                            description: "This schedule object overlaps with another one.",
                            relatedPuckIds: overlappingTasks.map((t) => t.buildPlanningTask),
                        },
                    ]
                })
            } else {
                ruleResults.push({
                    result: true,
                    scheduleObjectPrimaryKey: taskObject.buildPlanningTask
                })
            }
        }

        return ruleResults;
    }
}

Hi!

Few things to confirm here, so we can narrow down the space:

  1. On your Build Planning Rule object, you should have the following:
Property Type Class Kind Type Class Name
Primary Key schedules foo-rule
Title
Constraint Type schedules foo-rule-constraint
Function RID schedules foo-rule-function-id
Function Version schedules foo-rule-function-version
  1. You mentioned you have a M2M link to the schedule object (planning task). That is awesome! The typeclass schedules:schedulable-rule-link should be on both sides of that link.

  2. Confirm that your function rid and published function version (or version of function you want to appear…) are in the Build Planning Rule object. A given function’s rid and its published/latest versions can be found in a few places, one of which is OMA. In Ontology Manager Application (OMA), on the left side panel, there is a section “Functions”. Here you can search for any functions published. If your function is NOT in this list, this means you have not tagged/published it in Code Repos

Good questions! Just copy paste from docs.

I usually recommend having a separate types.ts (for ex) and declaring the interfaces here for clarity.