Logo ProcessCore

Decorations

The core data model is intentionally small: Dataset, Process, Recipe, Sample, Data, and Annotation describe the shape of a process graph. Domain specificity is added as decoration on top of that shared shape.

There are two complementary ways to do this:

  1. Use additionalType and additionalProperty on core objects. This keeps the data close to the ARC Core model and makes the extension queryable as typed Annotation annotations.
  2. Use the inherited DynamicObj property bag for information that must be preserved but does not fit into the core model.

This page shows both approaches.

Typed Decorations

The preferred extension path is to specialize core objects with additionalType and then attach ontologized Annotation records to the appropriate slot. The example below builds a small proteomics-style assay without introducing new graph node types.

let assay = Dataset("measurement1", additionalType = "Assay")
assay.Title <- Some "Proteomics assay"

let source =
    Sample("Base Culture", additionalType = "Source")

let organism =
    Annotation(
        "organism",
        value = "Arabidopsis thaliana",
        nameTAN = "https://bioregistry.io/SIO:010000",
        valueTAN = "https://bioregistry.io/NCBITaxon:3702",
        additionalType = "CharacteristicValue")

source.AddAdditionalProperty(organism)

let roomTemperatureSample =
    Sample("Cultivation Flask RT", additionalType = "Sample")

let temperature25 =
    Annotation(
        "temperature",
        value = "25",
        unit = "degree Celsius",
        nameTAN = "https://bioregistry.io/NCRO:0000029",
        unitTAN = "https://bioregistry.io/UO:0000027",
        additionalType = "FactorValue")

roomTemperatureSample.AddAdditionalProperty(temperature25)

let highTemperatureSample =
    Sample("Cultivation Flask HT", additionalType = "Sample")

let temperature30 =
    Annotation(
        "temperature",
        value = "30",
        unit = "degree Celsius",
        nameTAN = "https://bioregistry.io/NCRO:0000029",
        unitTAN = "https://bioregistry.io/UO:0000027",
        additionalType = "FactorValue")

highTemperatureSample.AddAdditionalProperty(temperature30)

let growthProtocol = Recipe(name = "Growth")
growthProtocol.AddComponent(
    Annotation(
        "growth environment",
        value = "bioreactor",
        nameTAN = "https://bioregistry.io/OBI:0000997",
        valueTAN = "https://bioregistry.io/OBI:0001046",
        additionalType = "Component"))

let growthAt25 = Process("Growth", executesRecipe = growthProtocol)
growthAt25.SetInputSample(source)
growthAt25.SetOutputSample(roomTemperatureSample)
assay.AddProcess(growthAt25)

let growthAt30 = Process("Growth", executesRecipe = growthProtocol)
growthAt30.SetInputSample(source)
growthAt30.SetOutputSample(highTemperatureSample)
assay.AddProcess(growthAt30)

let assayDecoration =
    [ "identifier", assay.Identifier
      "dataset additionalType", assay.AdditionalType |> valueOrBlank
      "processes", string assay.Processes.Count
      "samples", string (assay.AllSamples().Count)
      "data nodes", string (assay.AllData().Count) ]

assayDecoration
[("identifier", "measurement1"); ("dataset additionalType", "Assay");
 ("processes", "2"); ("samples", "3"); ("data nodes", "0")]

The dataset is still a Dataset, but additionalType = "Assay" tells downstream code which domain role it plays. The same pattern is used for sample roles: the input is a Source, while the outputs are Sample samples.

let sampleRoles =
    assay.AllSamples()
    |> Seq.countBy (fun sample -> sample.AdditionalType |> valueOrBlank)
    |> Seq.map (fun (role, count) -> role, count)
    |> Seq.toList

sampleRoles
[("Source", 1); ("Sample", 2)]

The first Growth process shows a compact ISA-style shape:

let growthInput =
    growthAt25.InputSample()
    |> Option.get

let growthOutput =
    growthAt25.OutputSample()
    |> Option.get

let growthDecoration =
    [ "process", growthAt25.Name
      "input", sprintf "%s (%s)" growthInput.Name (growthInput.AdditionalType |> valueOrBlank)
      "input annotations", growthInput.AdditionalProperty |> Seq.map pvSummary |> String.concat "; "
      "output", sprintf "%s (%s)" growthOutput.Name (growthOutput.AdditionalType |> valueOrBlank)
      "output annotations", growthOutput.AdditionalProperty |> Seq.map pvSummary |> String.concat "; "
      "protocol components", growthProtocol.Components |> Seq.map pvSummary |> String.concat "; " ]

growthDecoration
[("process", "Growth"); ("input", "Base Culture (Source)");
 ("input annotations", "CharacteristicValue: organism = Arabidopsis thaliana");
 ("output", "Cultivation Flask RT (Sample)");
 ("output annotations", "FactorValue: temperature = 25 degree Celsius");
 ("protocol components", "Component: growth environment = bioreactor")]

Process parameters use the same Annotation type, but they live on the Process.ParameterValue slot. Here, cell lysis records the sonicator, lysis duration, and technical replicate group as ParameterValue decorations.

let sonicator =
    Annotation(
        "sonicator",
        value = "Fisherbrand Model 705 Sonic Dismembrator",
        nameTAN = "https://bioregistry.io/OBI:0400114",
        valueTAN = "https://bioregistry.io/OBI:5453453",
        additionalType = "ParameterValue")

let lysisTime =
    Annotation(
        "time",
        value = "10",
        unit = "minute",
        nameTAN = "https://bioregistry.io/PATO:0000165",
        unitTAN = "https://bioregistry.io/UO:0000031",
        additionalType = "ParameterValue")

let technicalReplicate =
    Annotation(
        "technical replicate group",
        value = "1",
        nameTAN = "https://bioregistry.io/DPBO:1000184",
        additionalType = "ParameterValue")

let lysis = Process("Cell Lysis")
lysis.SetInputSample(roomTemperatureSample)
lysis.SetOutputSample(Sample("Eppi RT 1", additionalType = "Sample"))
lysis.AddParameterValue(sonicator)
lysis.AddParameterValue(lysisTime)
lysis.AddParameterValue(technicalReplicate)
assay.AddProcess(lysis)

lysis.ParameterValue
|> Seq.map pvSummary
|> Seq.toList
["ParameterValue: sonicator = Fisherbrand Model 705 Sonic Dismembrator";
 "ParameterValue: time = 10 minute";
 "ParameterValue: technical replicate group = 1"]

The practical benefit of this approach is that extensions remain easy to query. For example, all samples produced under the 25 degree Celsius growth factor can be found with ordinary F# sequence operations.

let samplesAt25Degrees =
    assay.AllSamples()
    |> Seq.filter (fun sample ->
        sample.AdditionalType = Some "Sample"
        && sample.AdditionalProperty
           |> Seq.exists (fun pv ->
               pv.AdditionalType = Some "FactorValue"
               && pv.Name = "temperature"
               && pv.Value = Some "25"))
    |> Seq.map (fun sample -> sample.Name)
    |> Seq.toList

samplesAt25Degrees
["Cultivation Flask RT"]

DynamicObj Extensions

All main ARC Core implementation classes inherit from DynamicObj. This gives each object a property bag for extension data that should be preserved, but that does not naturally belong in the process graph.

Use this for metadata such as facility layout, local tracking fields, UI state, or profile-specific fields that a core-only library should not interpret. The example below adds an experimental facility layout to a dataset.

let facilityDataset = Dataset("facility-layout-demo", additionalType = "Assay")
facilityDataset.Title <- Some "Greenhouse proteomics assay"

let environmentalControls = DynamicObj()
environmentalControls.SetProperty("temperatureSetpoint", "22 degree Celsius")
environmentalControls.SetProperty("relativeHumiditySetpoint", "60 percent")
environmentalControls.SetProperty("photoperiod", "16 h light / 8 h dark")

let facilityLayout = DynamicObj()
facilityLayout.SetProperty("facilityName", "Phytotron A")
facilityLayout.SetProperty("room", "Growth room 2")
facilityLayout.SetProperty("bench", "North bench")
facilityLayout.SetProperty("instrumentBay", "LC-MS bay 1")
facilityLayout.SetProperty("coordinateSystem", "room-grid")
facilityLayout.SetProperty("locationCode", "A-02-N-03")
facilityLayout.SetProperty("environmentalControls", environmentalControls)

facilityDataset.SetProperty("experimentalFacilityLayout", facilityLayout)

let recoveredFacility =
    facilityDataset.TryGetTypedPropertyValue<DynamicObj>("experimentalFacilityLayout")

let facilitySummary =
    match recoveredFacility with
    | Some layout ->
        [ "facility", layout.TryGetTypedPropertyValue<string>("facilityName") |> valueOrBlank
          "room", layout.TryGetTypedPropertyValue<string>("room") |> valueOrBlank
          "bench", layout.TryGetTypedPropertyValue<string>("bench") |> valueOrBlank
          "location", layout.TryGetTypedPropertyValue<string>("locationCode") |> valueOrBlank ]
    | None ->
        [ "facility", "missing" ]

facilitySummary
[("facility", "Phytotron A"); ("room", "Growth room 2");
 ("bench", "North bench"); ("location", "A-02-N-03")]

The YAML writer emits DynamicObj properties as overflow fields after the known ARC Core fields. This keeps the data round-trippable without requiring the core model to know what an experimentalFacilityLayout is.

let facilityYaml =
    ProcessCore.Yaml.Dataset.toYamlString (Some 2) facilityDataset
Show dataset YAML with DynamicObj extension
type: Dataset
identifier: facility-layout-demo
additionalType: Assay
title: Greenhouse proteomics assay
experimentalFacilityLayout:
  facilityName: Phytotron A
  room: Growth room 2
  bench: North bench
  instrumentBay: LC-MS bay 1
  coordinateSystem: room-grid
  locationCode: A-02-N-03
  environmentalControls:
    temperatureSetpoint: 22 degree Celsius
    relativeHumiditySetpoint: 60 percent
    photoperiod: 16 h light / 8 h dark

Read it back in lenient mode to preserve the extension field. Strict mode is for core-only documents and rejects unknown fields.

let roundTrippedFacility =
    ProcessCore.Yaml.Dataset.fromYamlString false facilityYaml

let roundTrippedLayout =
    roundTrippedFacility.TryGetTypedPropertyValue<DynamicObj>("experimentalFacilityLayout")

roundTrippedLayout.IsSome
true

What To Use When

Task

API

Give a core object a domain role

AdditionalType

Attach characteristics or factors to samples/data

node.AddAdditionalProperty

Attach process parameters

process.AddParameterValue

Attach protocol components

protocol.AddComponent

Keep extensions ontologized and queryable

Annotation(name, value, unit, nameTAN, valueTAN, unitTAN)

Preserve metadata outside the core graph

SetProperty, TryGetTypedPropertyValue from DynamicObj

Read/write decorated YAML

ProcessCore.Yaml.Dataset.fromYamlString false, toYamlString

Enforce core-only YAML

ProcessCore.Yaml.Dataset.fromYamlString true

namespace DynamicObj
namespace ProcessCore
val valueOrBlank: (string option -> string)
module Option from Microsoft.FSharp.Core
val defaultValue: value: 'T -> option: 'T option -> 'T
val pvSummary: pv: Annotation -> string
val pv: Annotation
Multiple items
type Annotation = inherit DynamicObj new: name: string * ?value: string * ?unit: string * ?nameTAN: string * ?valueTAN: string * ?unitTAN: string * ?additionalType: string * ?instanceOf: FormalParameter -> Annotation override Equals: obj: obj -> bool override GetHashCode: unit -> int member NameEquals: term: DefinedTerm -> bool member AdditionalType: string option with get, set member InstanceOf: FormalParameter option with get, set member Name: string with get, set member NameTAN: string option with get, set member NameText: string ...
<summary> Extensible key-value-unit triple. Primary extension mechanism of ProcessCore. schema.org/PropertyValue </summary>

--------------------
new: name: string * ?value: string * ?unit: string * ?nameTAN: string * ?valueTAN: string * ?unitTAN: string * ?additionalType: string * ?instanceOf: FormalParameter -> Annotation
val typeText: string
property Annotation.AdditionalType: string option with get, set
<summary> Subtype discriminator (e.g. ParameterValue, CharacteristicValue, FactorValue) </summary>
val valueText: string
property Annotation.ValueWithUnitText: string with get
val sprintf: format: Printf.StringFormat<'T> -> 'T
property Annotation.Name: string with get, set
val yamlCodeBlock: summary: string -> text: string -> string
val summary: string
val text: string
Multiple items
val string: value: 'T -> string

--------------------
type string = System.String
namespace System
namespace System.Net
type WebUtility = static member HtmlDecode: value: string -> string + 1 overload static member HtmlEncode: value: string -> string + 1 overload static member UrlDecode: encodedValue: string -> string static member UrlDecodeToBytes: encodedValue: byte array * offset: int * count: int -> byte array static member UrlEncode: value: string -> string static member UrlEncodeToBytes: value: byte array * offset: int * count: int -> byte array
<summary>Provides methods for encoding and decoding URLs when processing Web requests.</summary>
System.Net.WebUtility.HtmlEncode(value: string) : string
System.Net.WebUtility.HtmlEncode(value: string, output: System.IO.TextWriter) : unit
val assay: Dataset
Multiple items
type Dataset = inherit DynamicObj new: identifier: string * ?title: string * ?description: string * ?additionalType: string * ?license: string * ?datePublished: string * ?dateCreated: string * ?dateModified: string * ?processes: Process seq * ?hasPart: Dataset seq * ?dataFiles: Data seq * ?agents: Agent seq * ?citations: ScholarlyArticle seq * ?dataContexts: DataContext seq * ?additionalProperty: Annotation seq -> Dataset member AddAdditionalProperty: pv: Annotation -> unit member AddAgent: agent: Agent -> unit member AddCitation: article: ScholarlyArticle -> unit member AddDataContext: dataContext: DataContext -> unit member AddDataFile: data: Data -> unit member AddPart: child: Dataset -> unit member AddProcess: proc: Process -> unit member AllAgents: unit -> ResizeArray<Agent> ...
<summary> Container and context for data, processes, administrative metadata, and datamap entries. schema.org/Dataset </summary>

--------------------
new: identifier: string * ?title: string * ?description: string * ?additionalType: string * ?license: string * ?datePublished: string * ?dateCreated: string * ?dateModified: string * ?processes: Process seq * ?hasPart: Dataset seq * ?dataFiles: Data seq * ?agents: Agent seq * ?citations: ScholarlyArticle seq * ?dataContexts: DataContext seq * ?additionalProperty: Annotation seq -> Dataset
property Dataset.Title: string option with get, set
union case Option.Some: Value: 'T -> Option<'T>
val source: Sample
Multiple items
type Sample = inherit DynamicObj new: name: string * ?additionalType: string * ?additionalProperty: Annotation seq -> Sample member AddAdditionalProperty: pv: Annotation -> unit member AllAnnotations: ?scope: ResizeArray<Process> -> ResizeArray<Annotation> member AllConnectedNodes: ?scope: ResizeArray<Process> -> ResizeArray<IONode> member AllConnectedProcesses: ?scope: ResizeArray<Process> -> ResizeArray<Process> member ConnectedData: ?scope: ResizeArray<Process> -> ResizeArray<Data> member ConnectedSamples: ?scope: ResizeArray<Process> -> ResizeArray<Sample> member DownstreamAnnotations: ?recipeName: string * ?scope: ResizeArray<Process> -> ResizeArray<Annotation> member DownstreamData: ?scope: ResizeArray<Process> -> ResizeArray<Data> ...
<summary> Input or output biological, chemical, or digital sample in the process graph. bioschemas.org/Sample </summary>

--------------------
new: name: string * ?additionalType: string * ?additionalProperty: Annotation seq -> Sample
val organism: Annotation
member Sample.AddAdditionalProperty: pv: Annotation -> unit
val roomTemperatureSample: Sample
val temperature25: Annotation
type unit = Unit
val highTemperatureSample: Sample
val temperature30: Annotation
val growthProtocol: Recipe
Multiple items
type Recipe = inherit DynamicObj new: ?name: string * ?description: string * ?version: string * ?url: string * ?intendedUse: DefinedTerm * ?additionalType: string * ?parameters: FormalParameter seq * ?components: Annotation seq * ?additionalProperty: Annotation seq -> Recipe member AddAdditionalProperty: pv: Annotation -> unit member AddComponent: pv: Annotation -> unit member AddParameter: fp: FormalParameter -> unit override Equals: obj: obj -> bool override GetHashCode: unit -> int member RemoveAdditionalProperty: pv: Annotation -> unit member RemoveComponent: pv: Annotation -> unit member RemoveParameter: fp: FormalParameter -> unit ...
<summary> Description of a planned procedure. bioschemas.org/LabProtocol </summary>

--------------------
new: ?name: string * ?description: string * ?version: string * ?url: string * ?intendedUse: DefinedTerm * ?additionalType: string * ?parameters: FormalParameter seq * ?components: Annotation seq * ?additionalProperty: Annotation seq -> Recipe
member Recipe.AddComponent: pv: Annotation -> unit
val growthAt25: Process
Multiple items
type Process = inherit DynamicObj new: name: string * ?executesRecipe: Recipe * ?additionalType: string * ?input: IONode * ?output: IONode * ?parameterValue: Annotation seq -> Process member AddParameterValue: pv: Annotation -> unit member AnnotationsByName: name: string -> ResizeArray<Annotation> member CanonicalizeAllNodes: ds: Dataset -> unit member ClearInput: unit -> unit member ClearOutput: unit -> unit override Equals: obj: obj -> bool override GetHashCode: unit -> int member GetParameterValue: name: string -> Annotation ...
<summary> Core transformation node. Connects inputs to outputs by executing a recipe. bioschemas.org/LabProcess </summary>

--------------------
new: name: string * ?executesRecipe: Recipe * ?additionalType: string * ?input: IONode * ?output: IONode * ?parameterValue: Annotation seq -> Process
member Process.SetInputSample: m: Sample -> unit
member Process.SetOutputSample: m: Sample -> unit
member Dataset.AddProcess: proc: Process -> unit
val growthAt30: Process
val assayDecoration: (string * string) list
property Dataset.Identifier: string with get, set
property Dataset.AdditionalType: string option with get, set
<summary> Decoration discriminator (e.g. "Investigation", "Study", "Assay") </summary>
property Dataset.Processes: ResizeArray<Process> with get
property System.Collections.Generic.List.Count: int with get
member Dataset.AllSamples: unit -> ResizeArray<Sample>
member Dataset.AllData: unit -> ResizeArray<Data>
val sampleRoles: (string * int) list
module Seq from Microsoft.FSharp.Collections
val countBy: projection: ('T -> 'Key) -> source: 'T seq -> ('Key * int) seq (requires equality)
val sample: Sample
property Sample.AdditionalType: string option with get, set
<summary> Decoration discriminator (e.g. "Sample", "Source") </summary>
val map: mapping: ('T -> 'U) -> source: 'T seq -> 'U seq
val role: string
val count: int
val toList: source: 'T seq -> 'T list
val growthInput: Sample
member Process.InputSample: unit -> Sample option
val get: option: 'T option -> 'T
val growthOutput: Sample
member Process.OutputSample: unit -> Sample option
val growthDecoration: (string * string) list
property Process.Name: string with get, set
property Sample.Name: string with get, set
property Sample.AdditionalProperty: ResizeArray<Annotation> with get
module String from Microsoft.FSharp.Core
val concat: sep: string -> strings: string seq -> string
property Recipe.Components: ResizeArray<Annotation> with get
<summary> Equipment, reagents, and software used in this recipe (components). </summary>
val sonicator: Annotation
val lysisTime: Annotation
val technicalReplicate: Annotation
val lysis: Process
member Process.AddParameterValue: pv: Annotation -> unit
property Process.ParameterValue: ResizeArray<Annotation> with get
val samplesAt25Degrees: string list
val filter: predicate: ('T -> bool) -> source: 'T seq -> 'T seq
val exists: predicate: ('T -> bool) -> source: 'T seq -> bool
property Annotation.Value: string option with get, set
val facilityDataset: Dataset
val environmentalControls: DynamicObj
Multiple items
namespace DynamicObj

--------------------
type DynamicObj = inherit DynamicObject new: unit -> DynamicObj member DeepCopyProperties: ?includeInstanceProperties: bool -> obj member DeepCopyPropertiesTo: target: #DynamicObj * ?overWrite: bool * ?includeInstanceProperties: bool -> unit override Equals: o: obj -> bool override GetDynamicMemberNames: unit -> string seq override GetHashCode: unit -> int member GetProperties: includeInstanceProperties: bool -> KeyValuePair<string,obj> seq member GetPropertyHelpers: includeInstanceProperties: bool -> PropertyHelper seq member GetPropertyNames: includeInstanceProperties: bool -> string seq ...

--------------------
new: unit -> DynamicObj
member DynamicObj.SetProperty: propertyName: string * propertyValue: obj -> unit
val facilityLayout: DynamicObj
val recoveredFacility: DynamicObj option
member DynamicObj.TryGetTypedPropertyValue: propertyName: string -> 'TPropertyValue option
val facilitySummary: (string * string) list
val layout: DynamicObj
union case Option.None: Option<'T>
val facilityYaml: string
namespace ProcessCore.Yaml
module Dataset from ProcessCore.Yaml
val toYamlString: whitespace: int option -> ds: Dataset -> string
val roundTrippedFacility: Dataset
val fromYamlString: processCoreOnly: bool -> s: string -> Dataset
val roundTrippedLayout: DynamicObj option
property Option.IsSome: bool with get

Type something to start searching.