{"id":48531284,"url":"https://github.com/yiogmbh/fastbpmn","last_synced_at":"2026-04-18T10:01:49.769Z","repository":{"id":349701854,"uuid":"1198095602","full_name":"yiogmbh/fastbpmn","owner":"yiogmbh","description":"A framework allowing to write external tasks for various bpmn process engines.","archived":false,"fork":false,"pushed_at":"2026-04-07T06:03:18.000Z","size":818,"stargazers_count":0,"open_issues_count":2,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-07T07:25:21.658Z","etag":null,"topics":["async","asynchronous","asyncio-python","bpmn","python3"],"latest_commit_sha":null,"homepage":"https://yio.at","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/yiogmbh.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-01T05:47:43.000Z","updated_at":"2026-04-07T06:03:27.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/yiogmbh/fastbpmn","commit_stats":null,"previous_names":["yiogmbh/fastbpmn"],"tags_count":null,"template":false,"template_full_name":null,"purl":"pkg:github/yiogmbh/fastbpmn","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yiogmbh%2Ffastbpmn","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yiogmbh%2Ffastbpmn/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yiogmbh%2Ffastbpmn/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yiogmbh%2Ffastbpmn/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/yiogmbh","download_url":"https://codeload.github.com/yiogmbh/fastbpmn/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/yiogmbh%2Ffastbpmn/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31533824,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-07T16:28:08.000Z","status":"ssl_error","status_checked_at":"2026-04-07T16:28:06.951Z","response_time":105,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["async","asynchronous","asyncio-python","bpmn","python3"],"created_at":"2026-04-08T00:00:55.540Z","updated_at":"2026-04-08T00:01:24.301Z","avatar_url":"https://github.com/yiogmbh.png","language":"Python","readme":"# fastbpmn\nA framework allowing to write external tasks for various bpmn process engines.\n\n## Installation / Usage / Prerequisites\n\nEnsure that the libmagic C library is installed on your system. See [python-magic](https://pypi.org/project/python-magic/) for installation on various systems.\n\nOn OSX use:\n```\nbrew install libmagic\n```\n\n## Usage Example\n\n```python\nfrom fastbpmn import FastBPMN\nfrom fastbpmn.camunda import ProcessEngine\nfrom fastbpmn.models import BaseInputModel, BaseOutputModel\n\napp = FastBPMN(name=\"Bob\")\n\n\nclass OracleInput(BaseInputModel):\n    string_variable: str  # Requires a process variable called string_variable\n    integer_variable: int  # Requires a process variable called integer_variable\n\n\nclass OracleOutput(BaseOutputModel):\n    win_or_loose: bool  # Sets the process variable win_or_loose in the end\n\n\n@app.external_task(\n    topic=\"ask-oracle-delphi\",\n    input_class=OracleInput,\n    output_class=OracleOutput,\n)\nasync def ask_oracle_delphi(input_data: OracleInput):\n    \"\"\"\n    External Task / DEMO\n    Delphi was a sacred precinct that served as the seat of Pythia, the major oracle\n    who was consulted about important decisions throughout the ancient classical world.\n    \"\"\"\n    dummy_number = input_data.integer_variable + len(input_data.string_variable)\n\n    win_or_loose = bool(dummy_number % 2)\n\n    return OracleOutput(win_or_loose=win_or_loose)\n\n\n@app.external_task(\n    topic=\"ask-oracle-dodona\",\n    input_class=OracleInput,\n    output_class=OracleOutput,\n)\nasync def ask_oracle_dodona(input_data: OracleInput):\n    \"\"\"\n    External Task / DEMO\n    Dodona in Epirus in northwestern Greece was the oldest Hellenic oracle.\n    The earliest accounts in Homer describe Dodona as an oracle of Zeus.\n    \"\"\"\n    dummy_number = input_data.integer_variable + len(input_data.string_variable)\n\n    win_or_loose = not bool(dummy_number % 2)\n\n    return OracleOutput(win_or_loose=win_or_loose)\n\n\n# start the fastbpmn application using the included squirrel\nif __name__ == '__main__':\n    import squirrel\n    from structlog_config import configure_logger\n    log = configure_logger()\n\n    #structlog.stdlib.recreate_defaults(log_level=logging.INFO)\n\n    squirrel.run(\n        app,\n        flavour=\"camunda7\",\n\n        name=\"bob\",\n        workers=10,\n        camunda_url=\"\u003cyour pe url\u003e\",\n        camunda_username=\"\u003cyour username\u003e\",\n        camunda_password=\"\u003cyour password\u003e\",\n    )\n```\n\n#### Lifespan Handler\n\n```python\nfrom fastbpmn import FastBPMN\nfrom fastbpmn.camunda import ProcessEngine\nfrom contextlib import asynccontextmanager\n\n@asynccontextmanager\nasync def lifespan(app):\n    print(\"Done on startup ...\")\n    yield\n    print(\"done on shutdown ...\")\n\napp = FastBPMN(\n    name=\"Bob\",\n    lifespan=lifespan\n)\n```\n\n#### Retries\n\nIn order to handle errors with retries there is a special exception that should be\nraised within your external tasks. The latter example shows the usage:\n\n```python\nfrom fastbpmn import FastBPMN\nfrom fastbpmn.errors import RetryExternalTask\nfrom fastbpmn.camunda import ProcessEngine\n\napp = FastBPMN(\n    name=\"Bob\"\n)\n\n\n@app.external_task(\n    topic=\"last-forever\",\n    input_class=None,\n    output_class=None,\n)\nasync def retry_infinite(input_data: None) -\u003e None:\n    print(\"I will last forever\")\n    raise RetryExternalTask(retries=1)  # No matter what happens we always tell the\n    # process engine to try once again ;-)\n\n\n@app.external_task(\n    topic=\"try-5times\",\n    input_class=None,\n    output_class=None,\n    retries=5\n)\nasync def retry_infinite(input_data: None) -\u003e None:\n    print(\"You should see me 5 or 6 times ...\")\n    raise RetryExternalTask()  # not specifying a number of retries within the\n    # exception leads to decrease of initial number\n```\n\n#### No Input Values\n\nIt's possible to omit all the arguments if your external-task won't depend on input data.\n\n```python\nfrom fastbpmn import FastBPMN\nfrom fastbpmn.camunda import ProcessEngine\n\napp = FastBPMN(\n    name=\"Bob\"\n)\n\n\n@app.external_task(\n    topic=\"without-args\"\n)\nasync def no_args() -\u003e None:\n    print(\"I'm almighty, I won't need any arguments. I will work anyway\")\n    return\n```\n\n#### Task/TaskProperties\n\nSometimes you might be interested in properties of the current Task or the TaskProperties in general.\nYou can declare an external task such that you will receive this objects for usage:\n\n\u003e **Attention** This is highly experimental and due to upcoming refactorings of the process engine interface the\n\u003e Task and TaskProperties Class are likely to change in the future.\n\n```python\nfrom fastbpmn import FastBPMN\nfrom fastbpmn.task import Task, TaskProperties\nfrom fastbpmn.camunda import ProcessEngine\n\napp = FastBPMN(\n    name=\"Bob\"\n)\n\n\n@app.external_task(\n    topic=\"task-info\",\n    input_class=None,\n    output_class=None,\n)\nasync def print_taskinfo(input_data: None, task: Task, task_properties: TaskProperties) -\u003e None:\n    print(f\"TaskId: {task.id} - initial retries: {task_properties.retries}\")\n    return\n```\n\n#### Process\n\nA Process is a useful Method to create more structured code.\n\n```python\nfrom fastbpmn import FastBPMN, Process\nfrom fastbpmn.task import Task, TaskProperties\nfrom fastbpmn.camunda import ProcessEngine\n\napp = FastBPMN(\n    name=\"Bob\"\n)\n\nprocess_a = Process(\n    process_definition_key=\"ProzessA\"\n)\n\n\n@process_a.external_task(\"print_a\")\nasync def print_a(input_data: None) -\u003e None:\n    print(\"Hello from a Prozess A Only Task.\")\n    return\n\n\n# put me in a different file if you want ;-)\nprocess_b = Process(\n    process_definition_key=\"ProzessB\"\n)\n\n\n@process_b.external_task(\"print_b\")\nasync def print_b(input_data: None) -\u003e None:\n    print(\"Hello from a Prozess B Only Task.\")\n    return\n\n\n# You can also attach a TaskHandler method to multiple Processes\n\nasync def print_common(input_data: None, task: Task, task_properties: TaskProperties) -\u003e None:\n    print(f\"Hello from a common task, i was executed by process ... {task.process_definition_key}.\")\n    return\n\n\n# Add the print common to processes wherever you like\n# \u003e-\u003e The Topic can be different as well..\nprocess_b.add_task(\"print_common\", handler=print_common)\nprocess_a.add_task(\"print_common\", handler=print_common)\n\n\n```\n\n#### Context\n\nYou can make use of a context within your external task. The context provides some useful features\n(e.g. create of temporary files / directories).\n\n```python\nfrom fastbpmn import FastBPMN, Process\nfrom fastbpmn.context import Context, Delete\nfrom fastbpmn.camunda import ProcessEngine\n\napp = FastBPMN(\n    name=\"Bob\"\n)\n\nprocess_a = Process(\n    process_definition_key=\"ProzessA\"\n)\n\n\n@process_a.external_task(\"everlasting-file\")\nasync def print_a(ctx: Context) -\u003e None:\n    print(\"Hello from a Prozess A, I create a file that is not deleted ....\")\n    file_path = ctx.temp_file(flags=Delete.NEVER)\n    return\n\n\n@process_a.external_task(\"deleted-file\")\nasync def print_b(ctx: Context) -\u003e None:\n    print(\"Hello from a Prozess A, I create a file that deleted when I'm done ....\")\n    file_path = ctx.temp_file(flags=Delete.ALWAYS)\n    return\n```\n\n#### File Handling\n\nThere are several ways to deal with File variables in Camunda. The following example shows three use cases.\n\nThe assumption is the following:\n\n- the process has two file variables called `pdf_file_var` and `png_file_var` in camunda\n- there is an external task that needs to work with these files\n- the `png_file_var` is only optional and might not be present\n- there are three scenarios:\n    - the variable name in camunda are known\n    - the variable names in camunda are subject to change, but there are two other variables\n      holding the names (`pdf_file_var_name` and `png_file_var_name`)\n    - the variable names in camunda are subject to change as is the number of files, but there is\n      a list of variable names (`file_var_names`) that hold the names of the file variables\n\n```python\nimport asyncio\nfrom functools import cached_property\n\nfrom pydantic import computed_field, model_validator\n\nfrom fastbpmn import FastBPMN, Process\nfrom fastbpmn.context import Context\nfrom fastbpmn.camunda import ProcessEngine\nfrom fastbpmn.models import BaseInputModel, FileInfo, get_file_info_indirect\n\nprocess_a = Process(\n    process_definition_key=\"ProzessA\"\n)\napp = FastBPMN(name=\"Bob\")\n\n\nclass Option1InputModel(BaseInputModel):\n    \"\"\"\n    This is the first option to deal with file variables.\n\n    The variable names are known and can be used directly, that means there is a process\n    variable called `pdf_file_var` and `png_file_var` in camunda with type file.\n    \"\"\"\n    pdf_file_var: FileInfo\n    png_file_var: FileInfo | None = None\n\n\nclass Option2InputModel(BaseInputModel):\n    \"\"\"\n    This is the second option to deal with file variables.\n\n    The variable names are not known and are subject to change. But there are two other\n    variables (type string) that hold the names of the file variables.\n\n    Be aware of the computed properties and validators that are used to ensure that the\n    file variables are present as expected.\n    \"\"\"\n    # if `pdf_file_var_name` is set to 'other_pdf_var' then a process variable called\n    # `other_pdf_var` is expected to be present in camunda with type file. This variable\n    # is then used to compute the value of the computed_field `pdf_file`.\n    # if there is a process variable called pdf_file, then this variable is ignored (i guess)\n    pdf_file_var_name: str\n    png_file_var_name: str | None = None\n\n    @computed_field\n    @cached_property\n    def pdf_file(self) -\u003e FileInfo:\n        return get_file_info_indirect(self, self.pdf_file_var, required=True)\n\n    @computed_field\n    @cached_property\n    def png_file(self) -\u003e FileInfo | None:\n        return get_file_info_indirect(self, self.png_file_var, required=False)\n\n    @model_validator(mode='after')\n    def check_png_file(self):\n        \"\"\"\n        Checks if the file_info is present at the given key\n        \"\"\"\n        # this is a way to compute the value of the computed_field on initialization\n        # to trigger validation immediately\n        _ = self.png_file\n        return self\n\n    @model_validator(mode='after')\n    def check_pdf_file(self):\n        \"\"\"\n        Checks if the file_info is present at the given key\n        \"\"\"\n        # this is a way to compute the value of the computed_field on initialization\n        # to trigger validation immediately\n        _ = self.pdf_file\n        return self\n\n\nclass Option3InputModel(BaseInputModel):\n    \"\"\"\n    This is the third option to deal with file variables.\n\n    The variable names are not known and are subject to change. But there is a list of\n    variable names (type string) that hold the names of the file variables.\n    \"\"\"\n    file_var_names: list[str]\n\n    @computed_field\n    @cached_property\n    def files(self) -\u003e list[FileInfo]:\n        return [get_file_info_indirect(self, file_var_name, required=True) for file_var_name in self.file_var_names]\n\n    @model_validator(mode='after')\n    def check_files(self):\n        \"\"\"\n        Checks if the file_info is present at the given key\n        \"\"\"\n        # this is a way to compute the value of the computed_field on initialization\n        # to trigger validation immediately\n        _ = self.files\n        return self\n\n\n@process_a.external_task(\"option1\")\nasync def option1(ctx: Context, input_data: Option1InputModel) -\u003e None:\n    \"\"\"\n    This is the first option to deal with file variables.\n\n    The variable names are known and can be used directly, that means there is a process\n    variable called `pdf_file_var` and `png_file_var` in camunda with type file.\n    \"\"\"\n    pdf_file = await ctx.download_file(input_data.pdf_file)\n    if input_data.png_file:\n        png_file = await ctx.download_file(input_data.png_file)\n    # Or use gather ...\n\n    # ... do something with the files ...\n    return\n\n\n@process_a.external_task(\"option2\")\nasync def option2(ctx: Context, input_data: Option2InputModel) -\u003e None:\n    \"\"\"\n    This is the second option to deal with file variables.\n\n    See that implementation of the external task is the same as with option 1,\n    the difference is just that the variable names in camunda might differ as long\n    as there are string variables telling you the names of the file variables.\n    \"\"\"\n    pdf_file = await ctx.download_file(input_data.pdf_file)\n    if input_data.png_file:\n        png_file = await ctx.download_file(input_data.png_file)\n    # Or use gather ...\n\n    # ... do something with the files ...\n    return\n\n\n@process_a.external_task(\"option3\")\nasync def option3(ctx: Context, input_data: Option3InputModel) -\u003e None:\n    \"\"\"\n    This is the third option to deal with file variables.\n\n    See that implementation of the external task is the same as with option 1,\n    the difference is just that the variable names in camunda might differ as long\n    as there are string variables telling you the names of the file variables.\n    \"\"\"\n    files = await asyncio.gather(*[ctx.download_file(file) for file in input_data.files])\n    # ... do something with the files ... (the caveat is that you need to know the order and meaning of the files)\n    # but there might be use cases where this won't matter).\n    return\n\n```\n\n\n\n## Development\n\n```shell\n# setup evn ...\nuv sync\n\n# install commit hooks\nprek install\n```\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyiogmbh%2Ffastbpmn","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fyiogmbh%2Ffastbpmn","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fyiogmbh%2Ffastbpmn/lists"}