Skip to main content

Template Library

Introduction

This library provides a framework for building templates. One can create .tengo file, import this library, and call several functions, such as defineOutputs(), body() etc.

Terminology

A template is a resource representing a compiled text file with tengo code in Platforma Backend.

A template's renderer is a resource that represents a template with defined arguments.

A workflow controller waits for the template's renderer to become ready for execution and then runs the code with the specified arguments.

One can use render library to create templates' renderers.

There are two types of templates: simple and ephemeral.

A simple template waits until all its inputs are filled and only then is executed. Platforma Backend caches results of its execution, and the next time it encounters a new template's renderer with the same arguments, it retrieves the results from the cache and the renderer is not executed again.

As an example, running arbitrary commands with exec library is implemented with simple templates, and such commands won't be run twice.

Ephemeral template has much more control over when it should be executed. awaitState can be used to define a state of arguments suitable for running a specific template. For example, one can await a state where a particular argument must have a certain field with a value, while other fields do not need to exist.

For a more practical example, all workflows instantiated via workflow library are implemented as ephemeral templates. This is necessary because results from previous blocks can be received at different times: a slow result from a complex analysis pipeline and a fast result from the client's keyboard.

Sources

https://github.com/milaboratory/platforma/blob/main/sdk/workflow-tengo/src/tpl/index.lib.tengo

API

Import

One usually imports this library under alias self, since it represents an instance of this template.

self := import("@platforma-sdk/workflow-tengo:tpl")

defineOutputs

Defines current template output(s) names. For pure templates, all outputs must be defined before the template body, and only registered outputs can be used inside. Optional for ephemeral templates.

Arguments:

  • names: ...string - a list of output names.

Example:

self.defineOutputs("qc", "reports", "clones", "log", "clns")

body

Routes a template. One should provide a callback that will be executed when all of the arguments will be in ready state.

For simple templates it is when all of the arguments are ready, and for ephemeral templates it is a condition from awaitState().

Arguments:

  • bodyFn: func(inputs): map[string]outputs - the callback that gets all inputs as arguments and should return a map of all names to its outputs.

Example:

It's an example of defining a template and calling from another code using render library:

// caller template:
...
result := render.create(assets.importTemplate(":calleeTemplate"), {
argument1: 11,
argument2: 31,
nested: [{a: 1, b: 2}],
smartResourceOrField: resource1
})

result.output("result1", 24*60*60*1000) // == 42
...

// callee template:
self := import("@platforma-sdk/workflow-tengo:tpl")

self.defineOutputs("result1", "result2")

self.body(func(inputs) {
return {
// This will be 42
result1: inputs.argument1 + inputs.argument2,

// This will be a value that depends on a content of resource1 smart resource.
result2: inputs.resource1.getValue() + inputs.nested.a
}
})

prepare

Prepare additional resources for the template body. The body function of the workflow will only be executed when the references returned from the prepare method will be resolved into ready resources. The resulting resources will be passed as an input to the body function.

Arguments:

  • cb: func(inputs): map[string]reference - a callback function returning a map of references to be resolved

Example:

//
// We need to resolve the ref into the actual sequence set
// resource in order to use it in the body.
//
self.prepare(func(args) {
return {
resolvedRef: self.resolve(args.ref, { errIfMissing: true })
}
})

self.body(func(args) {
args.resolvedRef // this will have a resolved reference
})

inputs

Parses template inputs and returns a tengo map of them. The parser:

  • converts all json resources (potentially nested) into a tengo objects (maps)
  • converts all pl.maps (resource maps) into tengo maps

Return:

  • inputs: map[string]any - a map of the parsed template inputs

Example:

self.body(func(inputs) {
ll.assert(self.inputs() == inputs, "inputs and self.inputs() are both the same map")
})

rawInputs

It's the same as inputs, but doesn't parse all inputs and returns them as fields. It is useful when one need to pass map or value resources as-is and doesn't need parsed maps.

Return:

  • result: map[string]field: - a map of inputs to the fields of the input map.

hasInput

Shows whether the template's renderer has this input or not.

Return:

  • hasInput: bool: whether there is input with a given name

validateInputs

Validate inputs schema before passing inputs to body. See validation library for more details.

Arguments:

  • schema: validationSchema

awaitState

In an ephemeral template, body will only be executed when corresponding input state is reached.

Arguments:

  • path: ...string
  • requestedState: string

Allowed requested states:

  • Exists - field exists
  • Set - field points to existing resource
  • InputsLocked - field points to resource with locked inputs
  • AllInputsSet - field points to resource with all inputs set
  • ResourceReady - field points to ready resource

Example:

  • self.awaitState("AllInputsSet") - all inputs of the inputs map must be set.
  • self.awaitState("inputName1", "ResourceReady") - resource in the input inputName1 must be ready.
  • self.awaitState("inputName1", "nestedField", "Set") - nested input field in the input inputName1 must exist and point to some resource.
  • self.awaitState("inputName1", { wildcard: "*" }, "InputsLocked") - nested resource by the inputName1 field must exist and have locked inputs and all resources referenced by those fields must have all inputs locked.
  • self.awaitState("inputName1", { match: "^some_prefix\\." }, "InputsLocked") - nested resource by the inputName1 field must exist and have locked inputs and all resources referenced by inputs with must have all inputs locked.

Example, the following two expressions are equivalent:

  • self.awaitState("inputName1", { wildcard: "*" }, "Set")
  • self.awaitState("inputName1", { match: "^some_prefix\\." }, "Set")