Skip to main content

Run lightweight local workflows

Project description

Dyngle

An experimantal, lightweight, easily configurable workflow engine for automating development, operations, data processing, and content management tasks.

Technical foundations

  • Configuration, task definition, and flow control in YAML
  • Operations as system commands using a familiar shell-like syntax
  • Expressions and logic in pure Python

Quick installation (MacOS)

brew install python@3.11
python3.11 -m pip install pipx
pipx install dyngle

Getting started

Create a file .dyngle.yml:

dyngle:
  operations:
    hello:
      - echo "Hello world"

Run an operation:

dyngle run hello

Configuration

Dyngle reads configuration from YAML files. Specify the config file location using any of the following (in order of precedence):

  1. A --config command line option, OR
  2. A DYNGLE_CONFIG environment variable, OR
  3. .dyngle.yml in current directory, OR
  4. ~/.dyngle.yml in home directory

Operations

Operations are defined under dyngle: in the configuration. In its simplest form, an Operation is a YAML array defining the Steps, as system commands with space-separated arguments. In that sense, a Dyngle operation looks something akin to a phony Make target, a short Bash script, or a CI/CD job.

As a serious example, consider the init operation from the Dyngle configuration delivered with the project's source code.

dyngle:
  operations:
    init:
      - rm -rf .venv
      - python3.11 -m venv .venv
      - .venv/bin/pip install --upgrade pip poetry

The elements of the YAML array look like lines of Bash, but Dyngle processes them directly as system commands, allowing for template substitution and Python expression evaluation (described below). So shell-specific syntax such as |, >, and $VARIABLE won't work.

Data and Templates

Dyngle maintains a block of "Live Data" throughout an operation, which is a set of named values (Python dict, YAML "mapping"). The values are usually strings but can also be other data types that are valid in both YAML and Python.

The dyngle run command feeds the contents of stdin to the Operation as Data, by converting a YAML mapping to named Python values. The values may be substituted into commands or arguments in Steps using double-curly-bracket syntax ({{ and }}) similar to Jinja2.

For example, consider the following configuration:

dyngle:
  operations:
    hello:
      - echo "Hello {{name}}!"

Cram some YAML into stdin to try it in your shell:

echo "name: Francis" | dyngle run hello

The output will say:

Hello Francis!

Expressions

Operations may contain Expressions, written in Python, that can be referenced in Operation Step Templates using the same syntax as for Data. In the case of a naming conflict, an Expression takes precedence over Data with the same name. Expressions can reference names in the Data directly.

Expressions may be defined in either of two ways in the configuration:

  1. Global Expressions, under the dyngle: mapping, using the expressions: key.
  2. Local Expressions, within a single Operation, in which case the Steps of the operation require a steps: key.

Here's an example of a global Expression

dyngle:
  expressions:
    count: len(name)    
  operations:
    say-hello:
      - echo "Hello {{name}}! Your name has {{count}} characters."

For completeness, consider the following example using a local Expression for the same purpose.

dyngle:
  operations:
    say-hello:
      expressions:
        count: len(name)
      steps:
        - echo "Hello {{name}}! Your name has {{count}} characters."

Expressions can use a controlled subset of the Python standard library, including:

  • Built-in data types such as str()
  • Essential built-in functions such as len()
  • The core modules from the datetime package (but some methods such as strftime() will fail)
  • A specialized function called formatted() to perform string formatting operations on a datetime object
  • A restricted version of Path() that only operates within the current working directory
  • Various other useful utilities, mostly read-only, such as the math module
  • A special function called resolve which resolves data expressions using the same logic as in templates
  • An array args containing arguments passed to the dyngle run command after the Operation name

NOTE Some capabilities of the Expression namespace might be limited in the future. The goal is support purely read-only operations within Expressions.

Expressions behave like functions that take no arguments, using the Data as a namespace. So Expressions reference Data directly as local names in Python.

YAML keys can contain hyphens, which are fully supported in Dyngle. To reference a hyphenated key in an Expression, choose:

  • Reference the name using underscores instead of hyphens (they are automatically replaced), OR
  • Use the built-in special-purpose resolve() function (which can also be used to reference other expressions)
dyngle:
  expressions:
    say-hello: >-
        'Hello ' + full_name + '!'

... or using the resolve() function, which also allows expressions to essentially call other expressions, using the same underlying data set.

dyngle:
  expressions:
    hello: >-
        'Hello ' + resolve('formal-name') + '!'
    formal-name: >-
        'Ms. ' + full_name

Note it's also possible to call other expressions by name as functions, if they only return hard-coded values (i.e. constants).

dyngle:
  expressions:
    author-name: Francis Potter
    author-hello: >-
        'Hello ' + author_name()

Here are some slightly more sophisticated exercises using Expression reference syntax:

dyngle:
  operations:
    reference-hyphenated-data-key:
      expressions:
        spaced-name: "' '.join([x for x in first_name])"
        count-name: len(resolve('first-name'))
        x-name: "'X' * int(resolve('count-name'))"
      steps:
        - echo "Your name is {{first-name}} with {{count-name}} characters, but I will call you '{{spaced-name}}' or maybe '{{x-name}}'"
    reference-expression-using-function-syntax:
      expressions:
        name: "'George'"
        works: "name()"
        double: "name * 2"
        fails: double()
      steps:
        - echo "It works to call you {{works}}"
        # - echo "I have trouble calling you {{fails}}"

Finally, here's an example using args:

dyngle:
  operations:
    name-from-arg:
      expressions:
        name: "args[0]"
      steps:
        - echo "Hello {{name}}"

Passing values between Steps in an Operation

The Steps parser supports two special operators designed to move data between Steps in an explicit way.

  • The data assignment operator (=>) assigns the contents of stdout from the command to an element in the data
  • The data input operator (->) assigns the value of an element in the data (or an evaluated expression) to stdin for the command

The operators must appear in order in the step and must be isolated with whitespace, i.e.

<input-variable-name> -> <command and arguments> => <output-variable-name>

Here we get into more useful functionality, where commands can be strung together in meaningful ways without the need for Bash.

dyngle:
  operations:
    weather:
      - curl -s "https://api.open-meteo.com/v1/forecast?latitude=52.52&longitude=13.41&current_weather=true" => weather-data
      - weather-data -> jq -j '.current_weather.temperature' => temperature
      - echo "It's {{temperature}} degrees out there!"

If names overlap, data items populated using the data assignment operator take precedence over expressions and data in the original input from the beginning of the Operation.

Sub-operations

Operations can call other operations as steps using the sub: key. This allows for composability and reuse of operation logic.

Basic example:

dyngle:
  operations:
    greet:
      - echo "Hello!"
    
    greet-twice:
      steps:
        - sub: greet
        - sub: greet

Sub-operations can accept arguments using the args: key. The called operation can access these via the args array in expressions:

dyngle:
  operations:
    greet-person:
      expressions:
        person: "args[0]"
      steps:
        - echo "Hello, {{person}}!"
    
    greet-team:
      steps:
        - sub: greet-person
          args: ['Alice']
        - sub: greet-person
          args: ['Bob']

Scoping Rules

Sub-operations follow clear scoping rules that separate declared values from live data:

Declared Values are Locally Scoped:

  • Values and expressions declared via values: or expressions: keys are local to each operation
  • A parent operation's declared values are NOT visible to child sub-operations
  • A child sub-operation's declared values do NOT leak to the parent operation
  • Each operation only sees its own declared values plus global declared values

Live Data is Globally Shared:

  • Data assigned via the => operator persists across all operations
  • Live data populated by a sub-operation IS available to the parent after the sub-operation completes
  • This allows operations to communicate results through shared mutable state

Example demonstrating scoping:

dyngle:
  values:
    declared-val: global
  
  operations:
    child:
      values:
        declared-val: child-local
      steps:
        - echo {{declared-val}}  # Outputs "child-local"
        - echo "result" => live-data
    
    parent:
      steps:
        - echo {{declared-val}}  # Outputs "global"
        - sub: child
        - echo {{declared-val}}  # Still outputs "global"
        - echo {{live-data}}     # Outputs "result" (persisted from child)

Lifecycle

The lifecycle of an operation is:

  1. Load Data if it exists from YAML on stdin (if no tty)
  2. Find the named Operation in the configuration
  3. Perform template rendering on the first Step, using Data and Expressions
  4. Execute the Step in a subprocess, passing in an input value and populating an output value in the Data
  5. Continue with the next Step

Note that operations in the config are not full shell lines. They are passed directly to the system.

Imports

Configuration files can import other configuration files, by providing an entry imports: with an array of filepaths. The most obvious example is a Dyngle config in a local directory which imports the user-level configuration.

dyngle:
  imports:
    - ~/.dyngle.yml
  expressions:
  operations:

In the event of item name conflicts, expressions and operations are loaded from imports in the order specified, so imports lower in the array will override those higher up. The expressions and operations defined in the main file override the imports. Imports are not recursive.

MCP Server

Dyngle can run as an MCP (Model Context Protocol) server, exposing your configured operations as tools that can be used by AI assistants like Claude. This allows AI assistants to execute your Dyngle operations directly.

Starting the MCP Server

Run the MCP server using the mcp command:

dyngle mcp

By default, this starts a server using the stdio transport protocol, which is suitable for integration with clients like Claude Desktop.

The server also supports HTTP and SSE transports:

# Run with HTTP transport
dyngle mcp --transport http --host 127.0.0.1 --port 8000

# Run with SSE transport
dyngle mcp --transport sse --host 127.0.0.1 --port 8000

How It Works

When you start the MCP server:

  1. Each operation defined in your Dyngle configuration becomes an MCP tool
  2. AI assistants can discover and call these tools
  3. Tools accept:
    • data: A dictionary/JSON object (equivalent to data passed via stdin to dyngle run)
    • args: A list of arguments (equivalent to command-line arguments passed to dyngle run)
  4. Tools return a JSON response with:
    • stdout: Standard output from the operation
    • stderr: Standard error output from the operation
    • success: Boolean indicating whether the operation succeeded
    • exception: Error message if an exception occurred (null otherwise)

Configuring Claude Desktop

To use your Dyngle operations with Claude Desktop, you need to configure the MCP server in Claude's configuration file.

macOS:

Edit or create ~/Library/Application Support/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "dyngle": {
      "command": "dyngle",
      "args": ["mcp"]
    }
  }
}

Windows:

Edit or create %APPDATA%/Claude/claude_desktop_config.json:

{
  "mcpServers": {
    "dyngle": {
      "command": "dyngle",
      "args": ["mcp"]
    }
  }
}

Specifying a Configuration File:

If you want to use a specific Dyngle configuration file (other than .dyngle.yml in the current directory or ~/.dyngle.yml), you can specify it:

{
  "mcpServers": {
    "my-workflows": {
      "command": "dyngle",
      "args": ["--config", "/absolute/path/to/.dyngle.yml", "mcp"]
    }
  }
}

Important Notes:

  • You must use absolute paths in the configuration
  • After editing the configuration, fully quit and restart Claude Desktop (not just close the window)
  • The MCP tools will appear in Claude's "Search and tools" interface

Example Usage

Given this Dyngle configuration:

dyngle:
  operations:
    greet:
      - echo "Hello {{name}}!"
    
    weather:
      - curl -s "https://api.example.com/weather?city={{city}}" => weather-data
      - weather-data -> jq -r '.temperature' => temp
      - echo "The temperature in {{city}} is {{temp}} degrees"

After configuring Claude Desktop with your Dyngle MCP server, you can ask Claude:

  • "Use the greet tool with the name Alice"
  • "Check the weather for San Francisco"

Claude will execute your Dyngle operations and incorporate the results into its responses.

Troubleshooting

Server not showing up in Claude Desktop:

  1. Check that your configuration file has valid JSON syntax
  2. Ensure dyngle is in your PATH (run which dyngle on macOS/Linux or where dyngle on Windows)
  3. You may need to use the full path to the dyngle executable in the command field
  4. Fully quit and restart Claude Desktop (use Cmd+Q on macOS or quit from the system tray on Windows)

Checking logs (macOS):

Claude Desktop writes MCP-related logs to ~/Library/Logs/Claude/:

# View recent logs
tail -n 20 -f ~/Library/Logs/Claude/mcp*.log

Tool execution failures:

  • Check that your Dyngle configuration is valid by running operations manually first
  • Ensure any required data or arguments are being passed correctly
  • Review the stderr and exception fields in the tool response

Security

Commands are executed using Python's subprocess.run() with arguments split in a shell-like fashion. The shell is not used, which reduces the likelihood of shell injection attacks. However, note that Dyngle is not robust to malicious configuration. Use with caution.

MCP Server Security: When running as an MCP server, your operations can be executed by connected clients. Only expose operations that are safe for the intended use case, and be mindful of what operations you make available through the MCP interface.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dyngle-1.7.0.tar.gz (19.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dyngle-1.7.0-py3-none-any.whl (19.1 kB view details)

Uploaded Python 3

File details

Details for the file dyngle-1.7.0.tar.gz.

File metadata

  • Download URL: dyngle-1.7.0.tar.gz
  • Upload date:
  • Size: 19.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for dyngle-1.7.0.tar.gz
Algorithm Hash digest
SHA256 499ca0af2dedef76e1178c2bf484948b8a4a594ddf57ea0f96f76e3a03ae5038
MD5 6f2472517d6290da8b2c1374fbbf7120
BLAKE2b-256 b0d4a7388697abaf14e73664ff20fd4437c5f3a316e9112881bdcd37431a0b61

See more details on using hashes here.

File details

Details for the file dyngle-1.7.0-py3-none-any.whl.

File metadata

  • Download URL: dyngle-1.7.0-py3-none-any.whl
  • Upload date:
  • Size: 19.1 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.2.0 CPython/3.13.9

File hashes

Hashes for dyngle-1.7.0-py3-none-any.whl
Algorithm Hash digest
SHA256 07b0fe3dc2ad9bd39489e856dc90a247466780e42f16f35c2e59b9368ca7de16
MD5 48e22563cc894893185c8bce33f24efe
BLAKE2b-256 f4476a8b49a626e91db65c2dccaee1f763dab71fcdbfbeb40671cbd30b6b411e

See more details on using hashes here.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page