Rules
AKA Goal Planning
The task and rule constructs of Paraflow are used to provide "recipes" for how to achieve goals that match a particular pattern. In the case of rules, they define how to decompose a high-level goal into sub-goals and any sequencing dependencies.
Rules have a goal pattern and statement body. The following example is what a rule might look like for the !OnboardEmployee goal described in the previous section.
rule !OnboardEmployee($employee_id) plan {
parallel [
!ProcessW2($employee_id),
!DoCorporateTraining($employee_id)
];
}
This rule matches !OnboardEmployee goals. The $employee_id variable in the goal pattern is bound to the actual value of the matched goal and is used in the statement body to create two new sub-goals which can be performed in parallel, indicated by the separating comma. Goal statements separated by a semicolon create sub-goals that must be performed sequentially.
Goals that match rules are expanded recursively until no more rules match. The leaf goals of the expanded goal tree are the resulting actions to be performed, which are defined by tasks, described next.
Rule Declaration
The rule declaration has a goal pattern that describes what goals the rule can be applied to and a rule body that describes how to achieve that goal in terms of subgoals.
<rule> := "rule" <goal-pattern> "plan" <rule-body>
For example,
rule !DoAthenB($id) plan {
!DoA($id);
!DoB($id);
}
Rules may depend on data, but cannot change or create new data. Although subgoals may have data and time-order dependencies, the rule expansion into subgoals happens at once.
Rule Body
The rule body contains one or more statements that are executed to generate the subgoals that need to be achieved in order to achieve the matched head goal. The head goal is complete when all the generated subgoals are complete. If any subgoal fails, then the head goal fails.
The principal statement of the rule body is the goal statement, which consists of a single goal expression:
<plan-statement> := <plan-sequence> {"," <plan-sequence>} ";"
<plan-sequence> := <plan-primary> {"++" <plan-primary>}
<plan-primary> :=
<goal-instance> [ <send-chain> ] |
<goal-enablement> |
<plan-wait>
<goal-instance> := ["new"] <goal> <call-bindings> [ "?" ] [ <after-clause> ]
<goal-enablement> := <event> <call-bindings>
<plan-wait> := "wait" <duration-expression>
Goals instances separated by the , symbol have no ordering dependencies, and the planner will seek to achieve such instances independently and concurrently. Instances separated by ++ or in separate goal statements (separated by the semicolon) have an explicit ordering dependency, and the planner will not seek to achieve any such instance until all preceding instances are complete.
For example,
!shipPO(id -> $po), !invoicePO(id -> $po);
This example creates two subgoals that have no ordering dependencies.
Other statements such as control flow and loops may also be used in rule bodies. The full list of allowed statements is:
<rule-stmt-block> := "{" <rule-statement> { <rule-statement> } "}"
<rule-statement> :=
<simple-let-statement> |
<plan-statement> |
<if-statement> |
<with-statement> |
<loop-statement> |
<log-statement>
Passing Data
A goal may have named outputs associated with it. It is always possible to fetch those outputs explicitly within the rule and task declarations, however, the send operator provide a more readable way to transfer data between to goals in a sequence. The send syntax is:
<send-chain> := { "=>" <send-binding> <goal-instance> } [ output <send-binding> ]
For example,
!FindProvider($request_detail) => { provider: $source } !NewOrder($source, $request_detail);
The => operator creates a sequential dependency between the first and second goal, and also binds the value output by the first goal to an input of the second. In this case, the $source input is bound to the value of the provider output of !FindProvider. Since the second goal's input depends on the output of the first goal, the second goal cannot be planned until the first goal reaches the complete state.
Outputs
A task produce outputs using the return statement, for example return { $provider };
A rule can transfer outputs of its sub-goals to be an output of the rule with the output clause, as in this example:
rule !FindProvider($request_detail) plan {
!DiscoverProviders($request_detail); !SelectProvider($request_detail) output { $provider };
}
Here, the provider output of !SelectProvider is propagated as an output of !FindProvider if this rule is used.
Delay tasks
The wait step can be included in rule plans which behaves like a task that sleeps for the given duration. The wait can be used to introduce delay between to sequential goals. For example,
rule !HandleQuote($data) plan {
!SendQuote($data);
wait 1 day;
!QuoteFollowup($data);
}
A wait can also be useful for simulation:
rule !SimLongTask($data) plan {
wait 1 hour;
}
Opaque Goals
An opaque goal is a goal that is either achieved externally by the environment or one that cannot easily be described by a rule or task. The state of an opaque goal cannot be determined by the planner, and it's outcome is set via code. The assert, cancel and fail statements can be used to set the goal outcome within event or task bodies.
The most common use case for an opaque goal is for activities performed in external systems. For example, say a Paraflow actor is integrated with a ticket system like Jira and as part of it's workflow generates a ticket for some work to be done. The Paraflow actor does not have direct access to the ticket state and does known when it's completed. This scenario can be handled in Paraflow with an opaque goal that represents the ticket which gets asserted by an event posted by the external system when the ticket is complete (e.g. a WebHook).
rule !ExternalTask($id, $task) plan {
!CreateTicket($id, $task); !TicketComplete($id);
}
event handleWebhook($issue json) {
let {
fields: { user_id: $id, $state }
} = JsonParse($issue);
log info(`Ticket state $id -> $state`);
if $state == "Done" {
assert !TicketComplete(id -> $id);
}
}
The opaque goal is !TicketComplete and the parent goal !ExternalTask is not complete until the WebHook is called.
Just as a task can return outputs (see the Outputs section), the assert statement can also provide the output of an opaque goal. For example, assert !TicketComplete(id -> $id) output { $notes };
Spawned Goals
If a goal instance within a rule has includes the new keyword then it is spawned as an independent goal. Neither it's termination nor success or failure will affect the rule's goal.