ecm-generate-workflow-models

ecm-generate-workflow-models is a command-line tool that generates typed Python model classes for a workflow’s input variables — the static counterpart to workflow_model_by_name().

The generated classes map each input variable to a typed attribute (scalars to native Python types, lists to list[…​], records to their own fully typed nested classes). This gives IDE completion and type checking, and a model instance can be passed straight to start_process() — it serialises itself into the DataFields format the server expects.

1. Installation

The command ships with the main installation — no extra required:

  • uv

  • pip

uv add ecmind-blue-client
pip install ecmind-blue-client

2. Prerequisites

The tool reads workflows live from the server (wfm.GetWorkflowList to enumerate, wfm.GetWorkflow per workflow). Unlike ecm-generate-models, there is no --file source.

To enumerate the startable workflows the server requires two inputs:

  • a workflow-organisation user (not the security login). When --user is omitted, the tool resolves it from the login name (--username) via its Login attribute.

  • a client type (--client-type), as a GUID or a name such as OSECM_Client. The server requires exactly one client type; there is no "all clients" value.

3. Arguments

3.1. Server connection

Argument Default Description

--host HOST

enaio server hostname or IP. Required.

--port PORT

4000

TCP port of the enaio server.

--username USER

$ECM_USERNAME

Login username. Falls back to the ECM_USERNAME environment variable.

--password PASS

$ECM_PASSWORD

Login password. Falls back to the ECM_PASSWORD environment variable.

--no-ssl

SSL on

Disable SSL/TLS. Enabled by default.

--name APPNAME

Application name shown in Enterprise Manager (optional).

3.2. Workflow selection

Argument Default Description

--user ORG_USER_GUID

from --username

Workflow-organisation user GUID whose startable workflows are read. When omitted, resolved from the login name (--username) via its Login attribute. An explicit --user takes precedence.

--client-type CLIENT_TYPE

Client type as a GUID or name (e.g. OSECM_Client). Required server-side.

--organisation ORG

active org

Organisation GUID. Defaults to the active organisation.

--workflow MODEL_NAME

all

Only generate the workflow with this model name.

3.3. Output

Argument Default Description

--output-dir DIR

stdout

Directory to write the generated .py files into — one file per workflow. Without it, the code is printed to stdout.

4. Examples

4.1. Generate all startable workflows

The common case — --user is resolved from --username:

ecm-generate-workflow-models \
    --host enaio.example.com \
    --username admin \
    --password secret \
    --client-type "#OSECM_Client#" \
    --output-dir ./wf_models

Terminal output:

wrote wf_models/test.py

4.2. Credentials from the environment

export ECM_USERNAME=admin ECM_PASSWORD=secret
ecm-generate-workflow-models \
    --host enaio.example.com \
    --client-type "#OSECM_Client#" \
    --output-dir ./wf_models

4.3. A single workflow, preview to stdout

ecm-generate-workflow-models \
    --host enaio.example.com \
    --username admin \
    --password secret \
    --client-type "#OSECM_Client#" \
    --workflow test

4.4. Explicit org user

Takes precedence over login resolution:

ecm-generate-workflow-models \
    --host enaio.example.com \
    --username admin \
    --password secret \
    --user 46E29564BC464929A0A2ECA5387B4855 \
    --client-type "#OSECM_Client#" \
    --output-dir ./wf_models

5. Output format

One .py file is written per workflow. It defines an ECMWorkflowModel subclass with the input variables as typed attributes. Record variables are generated as their own fully typed ECMWorkflowRecordModel subclasses (recursively, including record-in-record and lists of records).

Example of a generated file:

# DO NOT EDIT — regenerate with generate_workflow_models.py (ecm-generate-workflow-models).

"""Generated typed workflow-variable model."""

from __future__ import annotations

from datetime import date, datetime, time

from ecmind_blue_client.ecm import ECMWorkflowModel, ECMWorkflowRecordModel  # base classes for typed access


class TestRecordVarRecord(ECMWorkflowRecordModel):
    """Generated record model."""

    StringField: str | None = None
    IntegerField: int | None = None
    TimeField: time | None = None
    DateTimeField: datetime | None = None
    FloatField: float | None = None
    StringListField: list[str] | None = None


class TestWorkflowModel(ECMWorkflowModel):
    """Input variables for the 'test' workflow."""

    test: str | None = None
    StringVar: str | None = None
    DateVar: date | None = None
    DatetimeVar: datetime | None = None
    StringListVar: list[str] | None = None
    FloatVar: float | None = None
    IntegerVar: int | None = None
    TimeVar: time | None = None
    TestRecordVar: TestRecordVarRecord | None = None

5.1. Type mapping

Server type Python type

STRING

str

INTEGER_SAFE

int

FLOAT_SAFE

float

DATE_SAFE

date

DATETIME_SAFE

datetime

TIME_SAFE

time

ListType

list[…]

RecordType

typed ECMWorkflowRecordModel nested class

By default only the input (INOUT) variables are mapped; internal fields are hidden.

6. Starting a workflow with generated models

Import the generated file and pass the model instance to start_process() as variables. The model serialises its typed values into the DataFields format itself (scalars as <String>, datetime as a Unix epoch, lists/records in the server format).

  • Sync

  • Async

import datetime

from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.pool import SyncPoolClient
from wf_models.test import TestWorkflowModel, TestRecordVarRecord

ecm = ECM(SyncPoolClient(servers="enaio.example.com:4000:1", username="admin", password="secret"))

# Resolve the workflow-organisation user from the login
user = ecm.workflow.organisation_user_by_login("admin")

# Fill the record variable type-safely
record = TestRecordVarRecord(
    StringField="x",
    IntegerField=1,
    TimeField="08:00:00",
    DateTimeField="1779365400",   # datetime member: Unix epoch
    FloatField="2.5",
    StringListField=["inner"],
)

# Model instance with all typed variables
variables = TestWorkflowModel(
    StringVar="Hello",
    IntegerVar=42,
    FloatVar=3.14,
    DateVar=datetime.date(2026, 6, 11),
    DatetimeVar=datetime.datetime(2026, 6, 11, 10, 30, 0),   # serialised as a Unix epoch
    TimeVar=datetime.time(10, 30, 0),
    StringListVar=["a", "b"],
    TestRecordVar=record,
)

# Start (organisation defaults to the active one)
process_id = ecm.workflow.start_process(
    user=user,
    workflow="test",
    client_type="#OSECM_Client#",
    variables=variables,
)
print("started process:", process_id)
import datetime

from ecmind_blue_client.ecm import ECM
from ecmind_blue_client.pool import AsyncPoolClient
from wf_models.test import TestWorkflowModel, TestRecordVarRecord

ecm = ECM(AsyncPoolClient(servers="enaio.example.com:4000:1", username="admin", password="secret"))

user = await ecm.workflow.organisation_user_by_login("admin")

record = TestRecordVarRecord(
    StringField="x", IntegerField=1, TimeField="08:00:00",
    DateTimeField="1779365400", FloatField="2.5", StringListField=["inner"],
)

variables = TestWorkflowModel(
    StringVar="Hello",
    IntegerVar=42,
    DatetimeVar=datetime.datetime(2026, 6, 11, 10, 30, 0),
    StringListVar=["a", "b"],
    TestRecordVar=record,
)

process_id = await ecm.workflow.start_process(
    user=user,
    workflow="test",
    client_type="#OSECM_Client#",
    variables=variables,
)
print("started process:", process_id)

6.1. Attaching files to the workflow

Documents are attached via the documents parameter. wfm.StartProcess references documents already archived in enaio by id/type and places them into the workflow file’s workspace — it does not upload new files. Create the DMS document first, then pass its id/type:

from ecmind_blue_client.ecm import ECMWorkspaceDocument
from ecmind_blue_client.rpc._job_request_file import JobRequestFileFromPath

# 1) Create a DMS document with a file (or use an existing one)
doc = ecm.dms.insert_and_get(
    MyDocumentModel(Name="Contract"),
    files=[JobRequestFileFromPath("contract.pdf")],
    folder_id=folder_id,
    register_id=register_id,
)

# 2) Pass its id/type when starting
process_id = ecm.workflow.start_process(
    user=user,
    workflow="test",
    client_type="#OSECM_Client#",
    variables=variables,
    documents=[ECMWorkspaceDocument(id=doc.system.id, type=doc.system.type_id)],
)

7. Without generation: runtime models

If static typing is not needed, the model can also be built at runtime without generation — workflow_model_by_name() returns the same class:

Model = ecm.workflow.workflow_model_by_name("test", user, "#OSECM_Client#")
record = Model.record_model("TestRecordVar")(StringField="x", IntegerField=1, ...)
variables = Model(StringVar="Hello", IntegerVar=42, TestRecordVar=record)

ecm.workflow.start_process(user=user, workflow="test",
                           client_type="#OSECM_Client#", variables=variables)

8. When to regenerate

Regenerate the files when:

  • a workflow’s input variables are added, removed, or retyped

  • record structures change

The files carry a DO NOT EDIT comment — manual edits are lost on the next generation.

9. See also