Run lightweight local workflows

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

Logo by Freepik-Flaticon

Technical Foundations

  • Configuration, task definition, and flow control in YAML - Define your workflows in declarative YAML files
  • Operations as system commands - Use familiar shell-like syntax for executing commands
  • Expressions and logic in pure Python - Leverage Python for dynamic values and logic

Key Features

  • Simple Operation Definition - Define workflows as arrays of system commands (learn more)
  • Data Flow Operators - Pipe data between steps with => and -> (learn more)
  • Template Substitution - Use {{variable}} syntax to inject data into commands (learn more)
  • Python Expressions - Evaluate Python code for dynamic values (learn more)
  • Sub-operations - Compose operations from other operations (learn more)
  • MCP Server Support - Expose operations as tools for AI assistants (learn more)

Use Cases

Dyngle is designed for:

  • Development workflow automation (build, test, deploy)
  • Operations tasks (server management, monitoring)
  • Data processing pipelines
  • Content management workflows
  • AI assistant tool integration

Read more

Quick Start

Get up and running with Dyngle in minutes.

Installation

Dyngle requires Python 3.13 or later.

On MacOS, try:

brew install pipx

Then:

pipx install dyngle

On Debian with Python already installed, try:

python3.13 -m pip install pipx
pipx install dyngle

In containers:

pip install dyngle

After installation, verify that Dyngle is working:

dyngle --help

You should see the command-line help output.

Getting Started

Create a file called .dyngle.yml in your current directory:

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

Run the operation:

dyngle run hello

You should see:

Hello world

Referencing CLI arguments

Operations can reference arguments passed in to the run command. Update your .dyngle.yml:

dyngle:
  operations:
    hello:
      - echo "Hello {{runtime.args.0}}!"

Run the operation:

dyngle run hello 'Ada'

You should see:

Hello Ada!

Referencing structured data

Command line arguments provide some convenience, but many operations require a more complete data structure. In its simplest form, input data handling can be tried by piping YAML text to the run command.

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

Run the operation:

echo "name: Katherine Johnson" | dyngle run hello

Output:

Hello Katherine Johnson!

Specifying an input data schema

With structured input data, it's possible to specify a schema which is validated when the operation runs. Use accepts: for that, and also move the commands a level down into a steps: entry.

dyngle:
  operations:
    hello:
      accepts:
        full-name:
          required: true
      steps:
        - echo "Hello {{full-name}}!"

This will fail:

echo "name: Jane" | dyngle run hello

But of course this works:

echo "full-name: Jane Goodall" | dyngle run hello

Performing logic with Python

Dyngle supports expressions that use Python, using a limited set of read-only operations.

The basic idea: Python for calculating, steps for doing things.

dyngle: 
  operations:
    hello:
      accepts:
        first-name:
        last-name:
          required: true
      expressions:
        full-name: (first_name if first_name else 'Ms.') + ' ' + last_name
      steps:
        - echo "Hello {{full-name}}!"
echo "last-name: Curie" | dyngle run hello
Hello Ms. Curie!

Creating AI tools

Dyngle includes an MCP server to run its operations. To use it, we need to direct the output from the operation to a variable, and return the variable, so the operation becomes like a function.

dyngle: 
  operations:
    hello:
      accepts:
        first-name:
        last-name:
      expressions:
        full-name: (first_name if first_name else 'Mx.') + ' ' + last_name
      steps:
        - echo "{{full-name}} says 'nice to meet you'." => greeting
      returns: greeting

To try it in Claude Desktop, edit or create ~/Library/Application Support/Claude/claude_desktop_config.json (or equivalent in OS's other than MacOS).

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

Then try a prompt:

Say hello to Jennifer Doudna using the Dyngle tool.

Read more

Operations

Operations are the fundamental building blocks in Dyngle. An operation is a named sequence of steps that execute commands, and/or a set of expressions to evaluate in realtime.

Basic Structure

Operations are defined under dyngle: in the configuration. The simplest form is a YAML array of command steps:

dyngle:
  operations:
    hello:
      - echo "Hello world"
    
    build:
      - npm install
      - npm run build

Run an operation:

dyngle run hello

Operation Definition

When you need additional attributes beyond just steps, use the extended form with a steps: key:

dyngle:
  operations:
    build:
      description: Build the project for production
      access: public
      returns: build-info
      expressions:
        build-time: "datetime.now()"
      steps:
        - npm install
        - npm run build
        - echo "{{build-time}}" => build-info

Omitting steps:

The steps: attribute is optional if all you need is expressions. This is convenient for simple operations that compute and return values without executing commands:

dyngle:
  operations:
    get-timestamp:
      returns: timestamp
      expressions:
        timestamp: "dtformat(datetime.now(), '%Y-%m-%d %H:%M:%S')"

Note that there are two kinds of steps - details in later sections:

  • Command steps (the default) - Execute system commands
  • Sub-operation steps - Call other operations

Operation Attributes

description

An optional description that appears in dyngle list-operations output:

dyngle:
  operations:
    deploy:
      description: Deploy to production
      steps:
        - sub: build
        - aws s3 sync ./dist s3://my-bucket/

access

Controls visibility and usage of operations.

public (default)

Public operations can be:

  • Run directly via dyngle run
  • Exposed as tools through the MCP server
  • Listed in dyngle list-operations output
dyngle:
  operations:
    deploy:
      access: public  # Explicitly public (default if omitted)
      description: Deploy to production
      steps:
        - sub: build
        - aws s3 sync ./dist s3://my-bucket/

If access: is not specified, operations default to public.

private

Private operations can only be called as sub-operations by other operations. They cannot be:

  • Run directly via dyngle run (will fail with an error)
  • Exposed through the MCP server
  • Listed in dyngle list-operations output
dyngle:
  operations:
    build:
      access: private
      steps:
        - npm install
        - npm run build
    
    deploy:
      steps:
        - sub: build  # OK - called as sub-operation
        - aws s3 sync ./dist s3://my-bucket/

Use private operations for:

  • Helper operations that shouldn't be run directly
  • Operations that handle secrets
  • Internal implementation details
  • Components of larger workflows

returns

Specifies what value to return. See Output modes for details.

dyngle:
  operations:
    get-temperature:
      returns: temp
      steps:
        - curl -s "https://api.example.com/weather" => weather-data
        - weather-data -> jq -r '.temperature' => temp

accepts

Defines a schema to validate and default input data. When specified, inputs (from stdin, send:, or JSON via MCP) are validated before execution.

Basic example:

dyngle:
  operations:
    greet-user:
      accepts:
        name: { type: string }
        age: { type: integer }
      steps:
        - echo "Hello {{name}}, age {{age}}"

The accepts: attribute creates a contract for your operation, ensuring it receives the expected data structure. See Inputs and interfaces for complete syntax and validation details.

expressions

Local expressions available only within this operation. See Constants and expressions for details.

dyngle:
  operations:
    greet:
      expressions:
        greeting: "'Hello ' + name + '!'"
      steps:
        - echo "{{greeting}}"

values

Local values (constants) available only within this operation. See Constants and expressions for details.

dyngle:
  operations:
    deploy:
      constants:
        environment: production
        region: us-west-2
      steps:
        - echo "Deploying to {{environment}} in {{region}}"

Operation Inputs

Inputs are values that enter an operation from outside. There are three ways inputs are provided to an operation:

Via stdin (in the run command)

Pass YAML data through stdin:

echo "name: Alice\nage: 30" | dyngle run greet-user

Via send: (in sub-operations)

Parent operations can pass data to sub-operations:

dyngle:
  operations:
    child:
      accepts:
        name: { type: string }
      steps:
        - echo "Hello {{name}}"
    
    parent:
      constants:
        user-data:
          name: Bob
      steps:
        - sub: child
          send: user-data

See Sub-operations for details.

Via JSON (through MCP)

When operations are called through the MCP server, inputs are provided as JSON:

{
  "tool": "greet-user",
  "name": "Alice",
  "age": 30
}

See MCP Server for details.

Examples

Development workflow

dyngle:
  operations:
    test:
      description: Run the test suite
      steps:
        - pytest --cov=src tests/
        - coverage report
    
    lint:
      description: Check code style
      steps:
        - black --check src/
        - flake8 src/
    
    ci:
      description: Run all checks
      steps:
        - sub: test
        - sub: lint

Data processing

dyngle:
  operations:
    process-data:
      returns: result
      steps:
        - curl -s "https://api.example.com/data" => raw-data
        - raw-data -> jq '.items' => filtered
        - filtered -> python process.py => result

Next Steps

Command Steps

Command steps are the default type of operation step in Dyngle. They execute system commands directly, without using a shell.

Not a Shell

Important: Operation steps use shell-like syntax but are not executed in a shell. Commands are parsed and executed directly by Python's subprocess.run().

This means shell-specific features won't work:

Doesn't work (shell syntax):

- echo "Hello" | grep Hello  # Use data flow operators instead
- export VAR=value           # Use expressions or values instead
- ls > files.txt             # Use data flow operators instead
- cd /some/path && ls        # Each step is independent

Works (Dyngle syntax):

- echo "Hello world"
- npm install
- python script.py --arg value
- curl -s "https://api.example.com/data"

Use Dyngle's features for data flow, variable substitution, and composability instead of shell features.

Template Syntax

Use double-curly-bracket syntax ({{ and }}) to inject values from the operation context into commands:

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

Templates are rendered before the command executes, replacing {{variable}} with the value from the operation context.

Using Templates

The initial context used for command steps may come from YAML over stdin to the Dyngle run command.

echo "name: Francis" | dyngle run hello

Output:

Hello Francis!

Nested Properties

The double-brackets may contain a context path, using dot-separated strings and ints to navigate nested dicts and lists.

dyngle:
  operations:
    weather-report:
      steps:
        - curl -s "https://api.example.com/weather" => weather
        - 'echo "Temperature: {{weather.temperature}}"'
        - 'echo "Location: {{weather.location.city}}, {{weather.location.country}}"'
dyngle:
  constants:
    python-command: /bin/python3
  expressions:
    unittest-command: format('{{python-command}}) -m unittest').split()
  operations:
    test:
      - '{{unittest-command.0}} {{unittest-command.1}} {{unittest-command.2}} test/*'

YAML type inference

YAML type inference impacts how Dyngle configurations are parsed. For example, if a string contains a colon, the YAML parser might interpret the content to the left of the colon as an object key.

- echo 'Temperature: {{temperature}}' => data  # YAML parsing error!

One solution is to enclose the entire string entry in single or double quotes.

- "echo 'Temperature: {{temperature}}' => data"

Another is to use a YAML multiline string.

- >-
  echo 'Temperature: {{temperature}}' => data

Remember that the YAML parser sends the entire string to Dyngle to parse, so the command and data flow operators must all occupy the same string entry in YAML.

Data Flow Operators

Dyngle provides two operators to pass data between steps.

The Send Operator (->)

The send operator passes a value from the operation context as stdin to a command:

dyngle:
  operations:
    process-data:
      steps:
        - curl -s "https://api.example.com/data" => raw-data
        - raw-data -> jq '.items' => filtered
        - filtered -> python process.py

The value is passed to the command's standard input.

The Receive Operator (=>)

The receive operator captures stdout from a command and assigns it to a named variable in the operation context:

dyngle:
  operations:
    fetch-data:
      steps:
        - curl -s "https://api.example.com/users" => users
        - echo "Fetched: {{users}}"

The captured value becomes available for subsequent steps.

Combining Operators

You can use both operators in a single step:

<input-variable> -> <command> => <output-variable>

Example:

dyngle:
  operations:
    weather:
      steps:
        - curl -s "https://api.example.com/weather" => weather-data
        - weather-data -> jq -j '.temperature' => temp
        - echo "Temperature: {{temp}} degrees"

Operator Order

When using both operators, they must appear in this order:

  1. Send operator (->) first
  2. Command in the middle
  3. Receive operator (=>) last

Operator Spacing

Operators must be isolated with whitespace:

Correct:

- command => output
- input -> command
- input -> command => output

Incorrect:

- command=>output        # Missing spaces
- input->command         # Missing spaces

Siilarity to Sub-operations

Note the similarity between these operators in command steps and the send: and receive: attributes in sub-operation steps:

Command step:

- input-data -> command => output-data

Sub-operation step:

- sub: operation-name
  send: input-data
  receive: output-data

Both follow the same pattern of sending input and receiving output. See Sub-operations for details.

Practical Examples

API Data Processing

dyngle:
  operations:
    get-user-emails:
      returns: emails
      steps:
        - curl -s "https://api.example.com/users" => users
        - users -> jq -r '.[].email' => emails

Multi-step Pipeline

dyngle:
  operations:
    analyze-logs:
      returns: summary
      steps:
        - curl -s "https://logs.example.com/today" => logs
        - logs -> grep "ERROR" => errors
        - errors -> wc -l => error-count
        - echo "Found {{error-count}} errors" => summary

Data Transformation

dyngle:
  operations:
    transform-json:
      returns: result
      steps:
        - cat input.json => raw
        - raw -> jq '.data | map({id, name})' => transformed
        - transformed -> python format.py => result

Important Notes

Variables Created with =>

Values populated with the receive operator (=>) have the highest precedence in the operation context. They override values, expressions, and inputs with the same name.

See Operation context for complete precedence rules.

Working with Structured Data

When data flow captures structured data (JSON, YAML), use a context path to access nested properties in templates:

dyngle:
  operations:
    weather:
      steps:
        - curl -s "https://api.example.com/weather" => weather-data
        - 'echo "Temperature: {{weather-data.temperature}}"'
        - 'echo "City: {{weather-data.location.city}}"'
dyngle:
  operations:
    process-items:
      expressions:
        items: from_json(get('items-json'))
      steps:
        - curl -s "https://api.example.com/items" => items-json
        - echo 'The first item is called {{items.0.name}}'

Using Expressions with Data Flow

You can reference captured data in expressions:

dyngle:
  operations:
    process:
      expressions:
        message: "format('Processed {{count}} items')"
      steps:
        - curl -s "https://api.example.com/items" => items
        - items -> jq 'length' => count
        - echo "{{message}}"

Prompt Steps

Prompt steps allow operations to pause and wait for user input, even when the operation receives data via stdin.

Basic Syntax

Use the prompt: key to display a message and wait for user input:

dyngle:
  operations:
    wait-for-user:
      steps:
        - prompt: "Press enter to continue"
        - echo "Continuing..."

Capturing Input

Use the receive: attribute to capture the user's input into a variable:

dyngle:
  operations:
    get-name:
      steps:
        - prompt: "Enter your name: "
          receive: user-name
        - echo "Hello {{user-name}}!"

The captured input becomes available in the operation context for use in subsequent steps.

Template Support

Prompt messages support template substitution:

dyngle:
  constants:
    app-name: "MyApp"
  operations:
    welcome:
      steps:
        - prompt: "Welcome to {{app-name}}! Press enter to start"
          receive: confirm
        - echo "Starting {{app-name}}..."

How It Works

Prompt steps use WizLib's UI handler to access the terminal (TTY) directly. This means prompts work even when stdin is redirected for passing data to the operation:

echo "config: value" | dyngle run my-operation

The operation can receive YAML data via stdin AND prompt the user for input during execution.

Use Cases

Interactive confirmation:

dyngle:
  operations:
    deploy:
      steps:
        - echo "About to deploy to production"
        - prompt: "Type 'yes' to confirm: "
          receive: confirmation
        - echo "Deploying..." # Only if user confirmed

Collecting user input:

dyngle:
  operations:
    setup:
      steps:
        - prompt: "Enter project name: "
          receive: project-name
        - prompt: "Enter author name: "
          receive: author
        - echo "Creating {{project-name}} by {{author}}"

Pausing for review:

dyngle:
  operations:
    analyze:
      steps:
        - curl -s "https://api.example.com/data" => data
        - echo "{{data}}"
        - prompt: "Review the data above. Press enter to continue"
        - data -> python process.py

Important Notes

TTY Required: Prompt steps require a TTY (interactive terminal). They will fail in non-interactive environments like cron jobs or CI/CD pipelines unless those environments provide TTY access.

stdin vs TTY: Operations can receive structured data via stdin (for YAML input) and still prompt users interactively. These are independent mechanisms:

  • stdin: For passing data to operations
  • TTY: For interactive user prompts

receive: Behavior: When using receive:, the captured input:

  • Can be an empty string if the user just presses enter
  • Becomes available immediately in the operation context
  • Has the same precedence as other variables created during execution

Next Steps

Operation Context

The operation context is all of the information available within an operation for use in templates, expressions, return:, and other places. Understanding the context is key to building effective operations.

What is Context?

Context encompasses all named values accessible within an operation, regardless of how they're defined or populated. The context includes:

  • Constants - Defined with constants:
  • Expressions - Defined with expressions:
  • Inputs - Data entering the operation from outside
  • Variables - Values set during execution with receive: or =>

Inputs

Inputs are values that enter the operation from outside sources. There are three ways inputs are provided:

Via stdin (in the run command)

Pass YAML data through stdin:

echo "name: Alice" | dyngle run hello

Via send: (in sub-operations)

Parent operations pass data to sub-operations:

dyngle:
  operations:
    child:
      steps:
        - echo "Hello {{name}}"
    
    parent:
      constants:
        data:
          name: Bob
      steps:
        - sub: child
          send: data  # 'name' becomes an input to child

Via JSON (through MCP)

When called through the MCP server, inputs are provided as JSON:

{
  "tool": "greet-user",
  "name": "Alice"
}

Via command-line arguments

Command-line arguments passed to dyngle run are available as the runtime.args list:

dyngle run greet Alice Bob Charlie

Note that runtime.args is not available directly to sub-commands; values must be passed explicitly.

Variables

Variables are values set during operation execution. They're created in two ways:

Using the receive operator (=>)

Capture command stdout:

dyngle:
  operations:
    fetch-data:
      steps:
        - curl -s "https://api.example.com/users" => users  # 'users' is a variable
        - echo "Fetched: {{users}}"

Using receive: in sub-operations

Capture return values from sub-operations:

dyngle:
  operations:
    get-version:
      returns: ver
      steps:
        - cat package.json -> jq -r '.version' => ver
    
    tag-release:
      steps:
        - sub: get-version
          receive: version  # 'version' is a variable
        - git tag "v{{version}}"

Variables have the highest precedence in the context (see below).

Constants and Expressions from Configuration

The context also includes values and expressions defined in your configuration:

Global

dyngle:
  constants:
    environment: production
  expressions:
    timestamp: "datetime.now()"
  operations:
    deploy:
      steps:
        - echo "Deploying to {{environment}} at {{timestamp}}"

Local (operation-specific)

dyngle:
  operations:
    deploy:
      constants:
        region: us-west-2
      expressions:
        build-time: "datetime.now()"
      steps:
        - echo "Deploying to {{region}} at {{build-time}}"

See Constants and expressions for details.

Context Key Precedence

When names overlap, Dyngle resolves them using this precedence (highest to lowest):

  1. Variables (populated via => operator or receive:) within the operation
  2. Local expressions and constants (defined in the operation)
  3. Global expressions and constants (defined under dyngle:)
  4. Inputs (from stdin, send:, or MCP JSON)

(Expressions and constants together are also called "declarations".)

Example:

dyngle:
  constants:
    name: Global
  expressions:
    name: "'Expression'"
  operations:
    test:
      constants:
        name: Local
      steps:
        - echo "Start: {{name}}"        # "Local" (local constant wins)
        - echo "Override" => name
        - echo "After: {{name}}"         # "Override" (variable wins)

Real-time Evaluation

Expressions are evaluated in real-time based on the current state of the context. This means:

  • When an expression references another value, it uses the current value at evaluation time
  • Variables created during execution are immediately available to subsequent expressions
  • Changes to the context during execution affect expression results

Example:

dyngle:
  operations:
    demo:
      expressions:
        message: "format('Count is {{count}}')"
      steps:
        - echo "5" => count
        - echo "{{message}}"        # "Count is 5"
        - echo "10" => count
        - echo "{{message}}"        # "Count is 10"

Accessing Context Values

Everything in the context can be referenced using context paths in these places:

In Templates

Use {{variable}} syntax in command steps:

- echo "Hello {{name}}!"
- 'echo "Temperature: {{weather.temperature}}"'

In Expressions with get()

Use the get() function to access context constants:

dyngle:
  expressions:
    full-greeting: "'Hello ' + get('name')"
    temp: get('weather.temperature')

In returns:

Specify which context value to return:

dyngle:
  operations:
    get-temperature:
      returns: temp
      steps:
        - curl -s "https://api.example.com/weather" => weather-data
        - weather-data -> jq -r '.temperature' => temp

The returns: key can reference:

  • Variables (set via => or receive:)
  • Constants (from constants:)
  • Expressions (from expressions:)
  • Inputs (from stdin, send:, or MCP)

Essentially, returns: can access anything in the operation context using the same key names.

Nested Object Properties

Access nested properties in dictionaries using context paths:

dyngle:
  operations:
    weather-report:
      steps:
        - curl -s "https://api.example.com/weather" => weather
        - 'echo "Temperature: {{weather.temperature}}"'
        - 'echo "Location: {{weather.location.city}}, {{weather.location.country}}"'

Working with Arrays

For arrays, use expressions to extract constants:

dyngle:
  constants:
    users:
      - name: Alice
        email: [email protected]
      - name: Bob
        email: [email protected]
  operations:
    show-users:
      expressions:
        first-user: get('users')[0]
        first-name: get('users')[0]['name']
        all-names: "[u['name'] for u in get('users')]"
      steps:
        - echo "First user: {{first-name}}"

Context Scope

Each operation maintains its own context. When using sub-operations:

  • Parent and child contexts are isolated by default
  • Use send: to explicitly pass data to a child
  • Use receive: to explicitly capture data from a child

See Sub-operations for details on context scope with sub-operations.

Example: Context Evolution

Here's how context evolves during execution:

dyngle:
  constants:
    base: Global constant
  expressions:
    computed: "'Global expression'"
  operations:
    demo:
      constants:
        local: Local constant
      expressions:
        derived: "'Derived: ' + get('local')"
      steps:
        - echo "{{base}}"           # Global constant
        - echo "{{computed}}"        # Global expression
        - echo "{{local}}"           # Local constant
        - echo "{{derived}}"         # Local expression
        - echo "Runtime" => dynamic  # Creates variable
        - echo "{{dynamic}}"         # Variable

Context at different points:

  1. Start: {base, computed, local, derived}
  2. After step 5: {base, computed, local, derived, dynamic}

Next Steps

Inputs and Interfaces

The accepts: attribute defines a schema for validating and defaulting operation inputs. This creates a clear contract for what data your operation expects.

Interface Definition

Define an interface using the accepts: key in your operation:

dyngle:
  operations:
    greet-user:
      accepts:
        name: { type: string }
        age: { type: integer }
      steps:
        - echo "Hello {{name}}, age {{age}}"

When inputs are provided (via stdin, send:, or MCP JSON), Dyngle validates them against this schema before executing the operation.

Basic Syntax

The interface syntax is loosely based on JSON Schema but with adaptations tailored for Dyngle's use cases. While it shares some conventions (like type names), it has its own behavior for required fields, defaults, and type inference.

Root Object

The interface always defines properties of a root object:

accepts:
  field1: { type: string }
  field2: { type: integer }

This expects input like:

field1: some text
field2: 42

Type System

Supported types:

  • string - Text values
  • integer - Whole numbers (no decimals)
  • number - Any numeric value (integers and floats)
  • boolean - True or false values
  • array - Lists of items
  • object - Nested structures with properties

Type Examples

dyngle:
  operations:
    demo:
      accepts:
        text: { type: string }
        count: { type: integer }
        price: { type: number }
        enabled: { type: boolean }
        tags: 
          type: array
          items: { type: string }
        config:
          type: object
          properties:
            host: { type: string }
            port: { type: integer }

Type Inference

Types can be inferred automatically based on other attributes:

accepts:
  name: {}  # Defaults to string
  message:  # Also defaults to string (YAML null value)
  user:
    properties:
      email: { type: string }  # Inferred as object from properties
  tags:
    items: { type: string }  # Inferred as array from items

Simplified syntax: You can omit the field value entirely (YAML null) or use an empty dict {} for string fields with blank defaults. These are equivalent:

accepts:
  name:  # YAML null - string with blank default
  title: {}  # Empty dict - string with blank default
  email: { type: string }  # Explicit string type

All three patterns above create optional string fields with blank string defaults.

Precedence for type inference:

  1. Explicit type: if declared
  2. object if properties: is present
  3. array if items: is present
  4. Otherwise string

Required Fields and Defaults

Field requirements vary by type:

  • String fields without explicit required or default get a blank string "" as the default (making them optional)
  • Other types are required by default unless marked required: false or given a default

Examples

accepts:
  name: {}  # String type, gets blank default "" if omitted
  nickname: { type: string, required: false }  # Also optional, no default
  age: { type: integer }  # Required (non-string type)
  email: { type: string, required: true }  # Explicitly required

Default Values

Provide explicit default values for optional fields:

accepts:
  name: { type: string }  # Gets blank string if omitted
  country:
    type: string
    default: "US"  # Gets "US" if omitted
  port:
    type: integer
    default: 8080  # Gets 8080 if omitted

Having an explicit default makes the field optional automatically.

Nested Objects

Define nested structures with properties:. The object type is automatically inferred from the presence of properties:, so you don't need to declare it explicitly:

dyngle:
  operations:
    process-order:
      accepts:
        customer:
          properties:  # object type inferred
            name:  # string type inferred (YAML null)
            email:
        shipping:
          properties:  # object type inferred
            address:
            city:
            zip:
      steps:
        - echo "Processing order for {{customer.name}}"
        - echo "Shipping to {{shipping.city}}"

You can nest objects arbitrarily deep:

accepts:
  order:
    properties:
      shipping:
        properties:
          address:
            properties:  # deeply nested object
              street:
              city:
              state:

Arrays

Define array types with items:

accepts:
  tags:
    type: array
    items: { type: string }
  scores:
    items: { type: integer }  # type: array inferred from items

Arrays of Objects

Combine arrays with nested objects. The array type is inferred from items:, and the object type for each item is inferred from properties::

accepts:
  items:
    items:  # array type inferred
      properties:  # object type inferred for each item
        product:  # string type inferred
        quantity: { type: integer }
        price: { type: number }

Validation Process

When an operation with accepts: is invoked:

  1. Input is received (from stdin, send:, or MCP JSON)
  2. Schema validation - Input structure is checked against the interface
  3. Type validation - Values are checked for correct types
  4. Defaults applied - Missing optional fields get their defaults
  5. Required fields checked - Missing required fields cause an error
  6. Execution proceeds - Validated input becomes available in the operation context

Validation Success

echo "name: Alice\nage: 30" | dyngle run greet-user
# Output: Hello Alice, age 30

Validation Failure

echo "age: 30" | dyngle run greet-user
# Error: Input validation failed for operation 'greet-user': 
# Field 'name' is required at root

Extra Fields

Extra fields are allowed by default:

echo "name: Bob\nage: 25\ncity: Seattle" | dyngle run greet-user  
# Output: Hello Bob, age 25 (city is ignored)

Complete Example

This example demonstrates all the key features including type inference, nested objects, arrays, defaults, and required fields:

dyngle:
  operations:
    create-user:
      description: Create a new user account
      accepts:
        username:  # string with blank default (optional)
        email: { type: string, required: true }  # explicitly required
        age: { type: integer }  # required (non-string type)
        role: { default: "user" }  # string with custom default
        preferences:
          properties:  # object type inferred
            theme: { default: "light" }
            notifications: { type: boolean, default: true }
        tags:
          items:  # array type inferred
            type: string
          default: []
      returns: result
      steps:
        - echo "Creating user {{username}} ({{email}})"
        - echo "Role: {{role}}, Age: {{age}}"
        - echo "Theme: {{preferences.theme}}"
        - echo "User created successfully" => result

Using Interfaces with Sub-operations

When using send: to pass data to sub-operations, the data is validated against the child operation's accepts: schema. This example shows nested objects being passed via send::

dyngle:
  operations:
    process-user:
      accepts:
        user:
          properties:  # object type inferred
            name:  # string type inferred
            email:
            age: { type: integer }
      steps:
        - echo "Processing {{user.name}}, age {{user.age}}"
    
    main:
      constants:
        user-data:
          user:
            name: Alice
            email: [email protected]
            age: 30
      steps:
        - sub: process-user
          send: user-data  # Nested structure validated against accepts schema

If validation fails, the parent operation stops with an error.

Using Interfaces with MCP

When operations are exposed via the MCP server, the accepts: schema determines the tool's input parameters:

With accepts:

dyngle:
  operations:
    get-weather:
      description: Get current weather for a city
      accepts:
        city: { type: string }
        units:
          type: string
          default: "metric"
      returns: weather-info
      steps:
        - curl -s "https://api.example.com/weather?city={{city}}&units={{units}}" => weather-info

The MCP tool will have city and units as input parameters, with validation and defaults applied automatically.

Without accepts:

dyngle:
  operations:
    run-backup:
      description: Run the nightly backup process
      returns: result
      steps:
        - /usr/local/bin/backup.sh => result

The MCP tool will have no input parameters.

See MCP Server for more details.

Best Practices

Use accepts for Public Operations

Operations exposed via MCP or called as sub-operations should define their interfaces. Use the simplified syntax for cleaner definitions:

dyngle:
  operations:
    deploy-service:
      description: Deploy a service to an environment
      accepts:
        service-name:  # string type inferred
        environment:
        version:
      steps:
        - echo "Deploying {{service-name}} v{{version}} to {{environment}}"

Provide Sensible Defaults

Use defaults for optional parameters to make operations easier to use:

accepts:
  environment:
    type: string
    default: "development"
  verbose:
    type: boolean
    default: false

Document with Descriptions

While Dyngle's interface syntax doesn't currently support field descriptions, use the operation's description: attribute to document expected inputs:

dyngle:
  operations:
    process-data:
      description: "Process data file. Expects: filename (string), format (json|csv), validate (boolean)"
      accepts:
        filename: { type: string }
        format: { type: string, default: "json" }
        validate: { type: boolean, default: true }

Next Steps

Constants and Expressions

Constants and expressions allow you to define reusable values in your configuration. Constants are static values, while expressions are dynamically evaluated using Python.

Constants

Define constants using the constants: key. Constants are static values that don't change during execution.

Global Constants

Defined under dyngle: and available to all operations:

dyngle:
  constants:
    environment: production
    region: us-west-2
    api-url: https://api.example.com
  operations:
    deploy:
      steps:
        - echo "Deploying to {{environment}} in {{region}}"

Local Constants

Defined within a specific operation:

dyngle:
  operations:
    greet:
      constants:
        greeting: Hello
        name: World
      steps:
        - echo "{{greeting}}, {{name}}!"

Local constants override global constants with the same name.

Expressions

Expressions are Python code snippets that compute dynamic values. They're evaluated in real-time using the Python interpreter with a controlled set of available functions and variables specific to Dyngle.

Basic Usage

Define expressions that evaluate to values:

dyngle:
  operations:
    greet:
      expressions:
        greeting: "'Hello ' + name + '!'"
      steps:
        - echo "{{greeting}}"

Run it:

echo "name: Alice" | dyngle run greet

Output:

Hello Alice!

Expressions vs Constants

An expression is like a constant that is evaluated dynamically:

dyngle:
  constants:
    static-time: "2024-01-01"  # Always the same
  expressions:
    current-time: "datetime.now()"  # Evaluated each time

Global Expressions

Defined under dyngle: and available to all operations:

dyngle:
  expressions:
    timestamp: "datetime.now()"
    author: "'Francis Potter'"
  operations:
    log:
      steps:
        - echo "[{{timestamp}}] Log by {{author}}"

Local Expressions

Defined within a specific operation:

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

Local expressions override global expressions with the same name.

Available Functions and Names

Expressions evaluate in a context that includes a subset of Python's standard features plus some Dyngle-specific functions. This controlled environment ensures expressions are powerful yet predictable.

Referencing Context Values

Values from the operation context can be referenced directly as Python variables:

dyngle:
  operations:
    greet:
      expressions:
        message: "'Hello ' + name"
      steps:
        - echo "{{message}}"

Hyphenated Names

YAML keys can contain hyphens. To reference them in expressions:

Option 1: Replace hyphens with underscores:

dyngle:
  operations:
    greet:
      expressions:
        message: "'Hello ' + first_name"  # References 'first-name'
      steps:
        - echo "{{message}}"

Option 2: Use the get() function:

dyngle:
  operations:
    greet:
      expressions:
        message: "'Hello ' + get('first-name')"
      steps:
        - echo "{{message}}"

Dyngle-Specific Functions

get()

Retrieve values from the operation context by name:

dyngle:
  expressions:
    full-greeting: "'Hello ' + get('first-name') + ' ' + get('last-name')"

The get() function can also reference other expressions:

dyngle:
  expressions:
    greeting: "'Hello'"
    full-greeting: "get('greeting') + ' ' + name"

format()

Render a template string using the current operation context:

dyngle:
  values:
    first-name: Alice
    last-name: Smith
  operations:
    greet:
      expressions:
        full-greeting: "format('Hello, {{first-name}} {{last-name}}!')"
      steps:
        - echo "{{full-greeting}}"

The format() function supports all template syntax, including nested properties:

dyngle:
  operations:
    weather-report:
      expressions:
        report: "format('Temperature in {{location.city}} is {{weather.temperature}} degrees')"
      steps:
        - echo "{{report}}"

dtformat()

Format datetime objects as strings:

dyngle:
  expressions:
    now: "datetime.now()"
    timestamp: "dtformat(get('now'), '%Y-%m-%d %H:%M:%S')"
  operations:
    log:
      steps:
        - echo "[{{timestamp}}] Event occurred"

PurePath()

Work with operating system paths:

dyngle:
  operations:
    git-dir:
      expressions:
        result: PurePath(cwd) / '.git'
      steps:
        - pwd => cwd
      returns: result   

Note that command steps can be used for I/O operations while using Python to manipulate paths and strings.

dyngle:
  operations:
    show-tests:
      expressions:
        test-dir: PurePath(cwd) / 'test'
        test-files: '[f.strip() for f in test_files_text.split("\n")]'
        result:
          test-dir: get('test-dir')
          test-files: get('test-files')
      steps:
        - pwd => cwd
        - ls -1 {{test-dir}} => test-files-text
      returns: result   

Runtime Declarations

Runtime declarations are automatically provided values that reflect runtime execution context. Unlike constants and expressions defined in your configuration, these are set by the Dyngle runtime based on how the operation was invoked.

runtime.args

Command-line arguments passed to dyngle run are available under runtime.args:

dyngle:
  operations:
    greet:
      expressions:
        name: "get('runtime.args.0') or 'World'"
      steps:
        - echo "Hello {{name}}!"

Run with:

dyngle run greet Alice

Output:

Hello Alice!

Important notes:

  • runtime.args is only available in the run command, not in MCP operations
  • Args are not automatically passed to sub-operations; use send: to pass them explicitly
  • Access via get('runtime.args.N') for safe access with defaults
  • Access via runtime.args.N in templates when you know the arg exists

The runtime namespace is reserved for future runtime-provided values (environment info, execution metadata, etc.).

Environment and System Functions

getenv()

Access environment variables:

dyngle:
  operations:
    show-env:
      expressions:
        home-dir: "getenv('HOME', '/default/path')"
        api-key: "getenv('API_KEY') or 'not-set'"
      steps:
        - 'echo "Home directory: {{home-dir}}"'
        - 'echo "API Key: {{api-key}}"'

getcwd()

Get the current working directory:

dyngle:
  operations:
    show-cwd:
      expressions:
        current-dir: "getcwd()"
        parent-dir: "str(PurePath(getcwd()).parent)"
      steps:
        - 'echo "Working in: {{current-dir}}"'
        - 'echo "Parent: {{parent-dir}}"'

Data Serialization Functions

to_json()

Convert Python data structures to JSON strings:

dyngle:
  operations:
    create-config:
      expressions:
        config-data:
          server: "format('{{host}}')"
          port: "int(get('port'))"
          enabled: "True"
        json-output: "to_json(get('config-data'))"
      steps:
        - echo "{{json-output}}"

from_json()

Parse JSON strings into Python data structures:

dyngle:
  operations:
    parse-json:
      expressions:
        parsed: "from_json(json_string)"
        server-host: "get('parsed')['server']"
      steps:
        - 'echo "Server: {{server-host}}"'

to_yaml()

Convert Python data structures to YAML strings:

dyngle:
  operations:
    create-yaml:
      expressions:
        config-data:
          database: "format('{{db-name}}')"
          timeout: "30"
        yaml-output: "to_yaml(get('config-data'))"
      steps:
        - echo "{{yaml-output}}"

from_yaml()

Parse YAML strings into Python data structures:

dyngle:
  operations:
    parse-yaml:
      expressions:
        parsed: "from_yaml(yaml_string)"
        db-name: "get('parsed')['database']"
      steps:
        - 'echo "Database: {{db-name}}"'

Python Features

Expressions support a subset of Python's features:

Built-in Types and Functions

  • str(), int(), float(), bool(), len(), etc.

Standard Library Modules

  • datetime - Date and time operations (datetime.now(), datetime.date(), etc.)
  • math - Mathematical functions (math.pi, math.sqrt(), etc.)
  • re - Regular expression operations (re.match(), re.search(), re.sub(), etc.)
  • PurePath() - Path manipulation operations (no file I/O)
  • json - JSON serialization via to_json() and from_json() functions
  • yaml - YAML serialization via to_yaml() and from_yaml() functions
  • os - Environment and system operations via getenv() and getcwd() functions

Data Structures

  • Lists, dictionaries, tuples
  • List comprehensions
  • Dictionary comprehensions

Operators

  • Arithmetic: +, -, *, /, //, %, **
  • Comparison: ==, !=, <, >, <=, >=
  • Logical: and, or, not
  • String: concatenation, formatting

Expression Examples

String Manipulation

dyngle:
  expressions:
    uppercase-name: "name.upper()"
    initials: "'.'.join([word[0] for word in name.split()])"

Mathematical Operations

dyngle:
  expressions:
    circle-area: "math.pi * radius ** 2"
    rounded: "round(get('circle-area'), 2)"

Date and Time

dyngle:
  expressions:
    now: "datetime.now()"
    today: "get('now').date()"
    formatted-date: "dtformat(get('now'), '%B %d, %Y')"

List Operations

dyngle:
  values:
    numbers: [1, 2, 3, 4, 5]
  expressions:
    doubled: "[n * 2 for n in get('numbers')]"
    sum-numbers: "sum(get('numbers'))"
    max-number: "max(get('numbers'))"

Conditional Logic

dyngle:
  expressions:
    environment: "get('env') if get('env') else 'development'"
    log-level: "'DEBUG' if get('environment') == 'development' else 'INFO'"

Nested Structure Syntax

Constants and expressions can contain YAML structures. This allows defining hierarchical data that can be referenced using context paths.

For Constants

Use nested YAML structures directly:

dyngle:
  constants:
    server:
      host: api.example.com
      port: 443
      ssl: true
    database:
      name: mydb
      connection:
        - host: db.example.com
        - port: 5432
  operations:
    connect:
      steps:
        - echo "Connecting to {{server.host}}:{{server.port}}"
        - 'echo "Database: {{database.name}}"'

For Expressions

Strings within YAML structures can be referenced and evaluated as separate expression:

dyngle:
  expressions:
    configure:
      server:
        host: "format('{{server-host}}')"
        port: "int(get('server-port'))"
      database:
        name: "format('{{db-name}}')"
        connection:
          - "format('{{db-host}}')"
          - "int(get('db-port'))"

Important Notes

  • Each string in an expression structure is evaluated as Python code
  • Numbers, booleans, and None pass through unchanged in both constants and expressions
  • For string literals in expressions, use Python string syntax: "'literal string'"
  • Access nested properties using context paths: {{config.server.host}}
  • Access array elements in expressions using Python brackets: get('coordinates')[get('location-index')]

Mixed Example

Combining constants and expressions with nested structures:

dyngle:
  constants:
    defaults:
      timeout: 30
      retries: 3
  expressions:
    runtime:
      timestamp: "datetime.now()"
      timeout: "get('defaults'.timeout') * 2"
      config:
        retry-count: "get('defaults.retries')"
        enabled: "True"
  operations:
    process:
      steps:
        - 'echo "Timeout: {{runtime.timeout}}"'
        - 'echo "Retries: {{runtime.config.retry-count}}"'

Context paths

Both constants and expressions with nested structures can be accessed using context paths in:

  • Templates: {{config.server.host}}
  • Expressions: get('config.server.host') or via variables if hyphen-free
  • returns:: config.server.host
dyngle:
  constants:
    api:
      endpoint: https://api.example.com
      version: v1
  operations:
    call-api:
      returns: api.endpoint
      expressions:
        full-url: "get('api.endpoint') + '/' + get('api.version')"
      steps:
        - echo "Calling {{full-url}}"

Next Steps

Sub-operations

Operations can call other operations as steps, enabling composability and code reuse. Sub-operation steps are the second type of operation step (alongside command steps).

Basic Usage

Use the sub: key to call another operation:

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

Passing Data with send:

To pass data to a sub-operation, use the send: attribute:

dyngle:
  operations:
    greet-person:
      steps:
        - echo "Hello, {{name}}!"
    
    main:
      constants:
        user:
          name: Alice
      steps:
        - sub: greet-person
          send: user  # Pass data to child

The data's keys and values become inputs in the sub-operation's context.

Capturing Results with receive:

When a sub-operation has a returns: key, capture its value with receive::

dyngle:
  operations:
    get-version:
      returns: ver
      steps:
        - cat package.json -> jq -r '.version' => ver
    
    tag-release:
      steps:
        - sub: get-version
          receive: version  # Capture return value
        - git tag "v{{version}}"

If the sub-operation has no returns: key, receive: stores None.

send: and receive: Together

Combine both to create function-like operations:

dyngle:
  operations:
    double:
      accepts:
        num: { type: integer }
      returns: result
      expressions:
        result: "num * 2"
      steps:
        - echo "Doubling {{num}}"
    
    main:
      constants:
        params:
          num: 5
      steps:
        - sub: double
          send: params      # Send input
          receive: doubled  # Capture output
        - echo "Result: {{doubled}}"

Similarity to Command Steps

Note the similarity between the send: and receive: attributes in sub-operation steps and the -> (send) and => (receive) operators in command steps:

Command step:

- input-data -> command => output-data

Sub-operation step:

- sub: operation-name
  send: input-data
  receive: output-data

Both follow the same pattern: data flows in via send/->, gets processed, and flows out via receive/=>.

Input Validation with accepts:

Define what data an operation accepts using accepts::

dyngle:
  operations:
    process-user:
      accepts:
        user-id: { type: string }
        email: { type: string }
      steps:
        - echo "Processing {{user-id}}: {{email}}"
    
    main:
      constants:
        user-data:
          user-id: "12345"
          email: [email protected]
      steps:
        - sub: process-user
          send: user-data  # Validated before execution

If the sent data doesn't match the accepts: schema, the operation fails with a clear error message. See Inputs and interfaces for complete details.

Operation Context Scope

Sub-operations are isolated by default - they do not automatically see the parent operation's context. This isolation makes operations predictable and testable - they behave like pure functions.

Isolation by Default

dyngle:
  operations:
    child:
      steps:
        - echo "{{parent-val}}"  # ERROR: parent-val not found
    
    parent:
      steps:
        - echo "secret" => parent-val
        - sub: child  # child cannot see parent-val

To share data between parent and child, use explicit send: and receive: attributes.

Constants and Expressions

Both declared constants (constants:/expressions:) and variables (=> assignments) are operation-local due to isolation:

Constants and expressions - Each operation sees only its own declarations plus globals:

dyngle:
  constants:
    global-val: Available to all
  
  operations:
    child:
      constants:
        local-val: Child only
      steps:
        - echo "{{global-val}}"  # OK - global
        - echo "{{local-val}}"   # OK - local to child
    
    parent:
      constants:
        parent-val: Parent only
      steps:
        - echo "{{global-val}}"   # OK - global
        - echo "{{parent-val}}"   # OK - local to parent
        - sub: child
        # child cannot see parent-val

Variables - Each operation maintains its own variables; => assignments don't cross boundaries:

dyngle:
  operations:
    child:
      steps:
        - echo "child-result" => data
        - echo "Child data: {{data}}"
    
    parent:
      steps:
        - echo "parent-result" => data
        - echo "Parent data: {{data}}"  # "parent-result"
        - sub: child
        - echo "After child: {{data}}"  # Still "parent-result" (child's data isolated)

Complete Isolation Example

dyngle:
  constants:
    declared-val: global
  
  operations:
    child:
      constants:
        declared-val: child-local
      steps:
        - echo "{{declared-val}}"  # "child-local" (own declaration)
        - echo "result" => live-data
    
    parent:
      steps:
        - echo "{{declared-val}}"  # "global" (no local override)
        - echo "parent" => live-data
        - sub: child
        - echo "{{declared-val}}"  # Still "global"
        - echo "{{live-data}}"     # Still "parent" (child's data isolated)

Data Sharing

Use send: and receive: for explicit parent-child data flow:

dyngle:
  operations:
    child:
      returns: result
      steps:
        - echo "Processing {{input-value}}"
        - echo "done" => result
    
    parent:
      constants:
        data:
          input-value: hello
      steps:
        - sub: child
          send: data       # Explicitly share data
          receive: output  # Explicitly capture result
        - echo "Got: {{output}}"

Use Cases

Build Pipeline

dyngle:
  operations:
    install-deps:
      - npm install
    
    compile:
      - npm run build
    
    test:
      - npm test
    
    build:
      description: Full build pipeline
      steps:
        - sub: install-deps
        - sub: compile
        - sub: test

Reusable Components with Private Operations

dyngle:
  operations:
    setup-env:
      access: private
      steps:
        - echo "Setting up environment..."
        - export NODE_ENV=production
    
    deploy-frontend:
      description: Deploy frontend application
      steps:
        - sub: setup-env
        - npm run deploy:frontend
    
    deploy-backend:
      description: Deploy backend services
      steps:
        - sub: setup-env
        - npm run deploy:backend

Data Processing Pipeline

dyngle:
  operations:
    fetch-data:
      returns: raw
      steps:
        - curl -s "https://api.example.com/data" => raw
    
    transform-data:
      accepts:
        input: { type: string }
      returns: output
      steps:
        - input -> jq '.items' => output
    
    process-all:
      returns: final
      steps:
        - sub: fetch-data
          receive: data
        - sub: transform-data
          send: payload
          receive: final
      constants:
        payload:
          input: "{{data}}"

Helper Operations for Secrets

Prevent accidental exposure of operations that handle secrets:

dyngle:
  operations:
    get-api-token:
      access: private
      returns: token
      steps:
        - aws secretsmanager get-secret-value --secret-id api-token => secret
        - secret -> jq -r '.SecretString' => token
    
    call-api:
      description: Make authenticated API call
      steps:
        - sub: get-api-token
          receive: token
        - curl -H "Authorization: Bearer {{token}}" https://api.example.com/data

This prevents running dyngle run get-api-token accidentally or exposing it through the MCP server.

Multi-Step Workflows with Composition

Build complex workflows from smaller private operations:

dyngle:
  operations:
    install-dependencies:
      access: private
      - npm install
    
    run-tests:
      access: private
      - npm test
    
    build-artifacts:
      access: private
      - npm run build
    
    upload-artifacts:
      access: private
      - aws s3 sync ./dist s3://my-bucket/
    
    ci-pipeline:
      description: Run full CI/CD pipeline
      steps:
        - sub: install-dependencies
        - sub: run-tests
        - sub: build-artifacts
        - sub: upload-artifacts

Users only see and can run ci-pipeline, not the internal helpers.

Best Practices

Use accepts: for Clear Contracts

Define what data your operations need:

dyngle:
  operations:
    deploy-service:
      accepts:
        service-name: { type: string }
        version: { type: string }
        environment: { type: string }
      steps:
        - echo "Deploying {{service-name}} v{{version}} to {{environment}}"

This serves as self-documentation and catches errors early.

Explicit is Better Than Implicit

Always use send: and receive: for data flow:

Good:

dyngle:
  operations:
    get-version:
      returns: version
      steps:
        - cat package.json -> jq -r '.version' => version
    
    tag-release:
      steps:
        - sub: get-version
          receive: ver
        - git tag "v{{ver}}"

Avoid (this won't work due to isolation):

dyngle:
  operations:
    get-version:
      steps:
        - cat package.json -> jq -r '.version' => version
    
    tag-release:
      steps:
        - sub: get-version
        - git tag "v{{version}}"  # ERROR: version not found

Use Private Operations for Helpers

Mark helper operations as private to prevent direct execution:

dyngle:
  operations:
    deploy:
      description: Deploy the application
      steps:
        - sub: validate
        - sub: build
        - sub: upload
    
    validate:
      access: private
      steps:
        - echo "Validating configuration..."
    
    build:
      access: private
      steps:
        - npm run build
    
    upload:
      access: private
      steps:
        - aws s3 sync ./dist s3://my-bucket/

Make Public Operations User-Facing

Public operations should represent complete, user-facing actions, while private operations are focused, reusable components.

Next Steps

Output Modes

Operations behave differently depending on whether they have a returns: key. This determines how output is handled and displayed.

The Two Modes

Script Mode (without returns:)

Operations without returns: behave like shell scripts - all command stdout goes to stdout:

dyngle:
  operations:
    build:
      - echo "Starting build..."
      - npm install
      - npm run build
      - echo "Build complete!"

All output is visible, making these ideal for build, deploy, and other workflow tasks where you want to see progress.

Function Mode (with returns:)

Operations with returns: behave like functions - command stdout is suppressed, and only the return value is provided as output:

dyngle:
  operations:
    get-temperature:
      returns: temp
      steps:
        - echo "Fetching weather..."  # stdout suppressed
        - curl -s "https://api.example.com/weather" => weather-data
        - weather-data -> jq -r '.temperature' => temp

Only the return value is output, making these ideal for data queries and transformations.

Important:

  • stderr is always displayed in both modes
  • The => operator works in both modes (capturing stdout to a variable)

Output Destinations

Where the output goes depends on how the operation is invoked.

In the run Command

When running an operation with dyngle run, output goes to stdout.

Script mode - All command stdout is displayed:

dyngle run build

Output:

Starting build...
[npm install output...]
[npm run build output...]
Build complete!

Function mode - Only the return value is displayed:

dyngle run get-temperature

Output:

72

Output formatting depends on the return value type:

  • Simple types (strings, numbers, booleans) - Printed as-is
  • Dictionaries and lists - Formatted as YAML

Example with structured data:

dyngle:
  operations:
    get-user:
      returns: user
      steps:
        - curl -s "https://api.example.com/user/123" => user
dyngle run get-user

Output:

name: Alice Smith
email: [email protected]
role: admin

In Sub-operations

When an operation is called as a sub-operation, its return value is captured by the parent's receive: attribute:

dyngle:
  operations:
    get-version:
      returns: version
      steps:
        - cat package.json -> jq -r '.version' => version
    
    tag-release:
      steps:
        - sub: get-version
          receive: ver
        - git tag "v{{ver}}"
        - git push origin "v{{ver}}"

The return value becomes a variable in the parent operation's context.

If no returns: is specified, receive: captures None.

In the MCP Server

When operations are exposed via the MCP server, they return JSON responses.

Success with return value:

dyngle:
  operations:
    get-weather:
      description: Get current weather for a city
      returns: weather-info
      accepts:
        city: { type: string }
      steps:
        - curl -s "https://api.example.com/weather?city={{city}}" => weather-info

Response:

{
  "result": {
    "temperature": 72,
    "conditions": "Sunny",
    "humidity": 65
  }
}

Success without return value:

dyngle:
  operations:
    run-backup:
      description: Run the nightly backup process
      steps:
        - /usr/local/bin/backup.sh

Response:

{
  "result": null
}

Error:

{
  "error": "Operation failed: command not found"
}

This makes operations with return values particularly useful as AI assistant tools - they can return structured data that assistants can incorporate into responses.

Specifying Return Values

Use the returns: key to specify what value to return:

dyngle:
  operations:
    get-temperature:
      returns: temp
      steps:
        - curl -s "https://api.example.com/weather" => weather-data
        - weather-data -> jq -r '.temperature' => temp

The returns: key can reference anything in the operation context using context paths:

  • Variables (set via => or receive:)
  • Constants (from constants:)
  • Expressions (from expressions:)
  • Inputs (from stdin, send:, or MCP)
  • Nested properties (using dot-separated context paths)

Examples:

dyngle:
  operations:
    # Return a variable
    fetch-data:
      returns: data
      steps:
        - curl -s "https://api.example.com" => data
    
    # Return a constant
    get-env:
      returns: environment
      constants:
        environment: production
    
    # Return an expression
    get-timestamp:
      returns: timestamp
      expressions:
        timestamp: "dtformat(datetime.now(), '%Y-%m-%d %H:%M:%S')"
    
    # Return nested property
    get-host:
      returns: config.server.host
      constants:
        config:
          server:
            host: api.example.com
            port: 443

Display Control

The --display option controls whether step commands are shown before execution, independently of output mode:

dyngle run build --display none

This suppresses step display but doesn't affect the mode (script vs function) or stdout handling.

See CLI Commands for details on the --display option.

Examples

Simple String Return

dyngle:
  operations:
    get-version:
      returns: version
      steps:
        - cat package.json -> jq -r '.version' => version
dyngle run get-version

Output:

1.2.3

Structured Data Return

dyngle:
  operations:
    system-info:
      returns: info
      expressions:
        info:
          hostname: "get('runtime.args.0') or 'localhost'"
          timestamp: "datetime.now()"
          user: "'admin'"
dyngle run system-info myserver

Output:

hostname: myserver
timestamp: 2024-12-14 22:00:00
user: admin

Computed Return Value

dyngle:
  operations:
    calculate-total:
      returns: total
      steps:
        - curl -s "https://api.example.com/items" => items
        - items -> jq '[.[] | .price] | add' => total

Workflow with No Return

dyngle:
  operations:
    deploy:
      description: Deploy to production
      steps:
        - echo "Starting deployment..."
        - sub: build
        - aws s3 sync ./dist s3://my-bucket/
        - echo "Deployment complete!"

All output is visible during execution.

Best Practices

Use Function Mode for Data Operations

Operations that query or transform data should use returns::

dyngle:
  operations:
    get-status:
      description: Check deployment status
      returns: status
      steps:
        - curl -s "https://api.example.com/status" => status

Use Script Mode for Workflows

Operations that perform tasks should omit returns::

dyngle:
  operations:
    deploy:
      description: Deploy application
      steps:
        - sub: build
        - sub: test
        - sub: upload
        - echo "Deployment complete!"

Return Structured Data for MCP

Operations exposed via MCP should return meaningful structured data:

dyngle:
  operations:
    check-health:
      description: Check application health
      returns: health
      steps:
        - curl -s "https://api.example.com/health" => health

This allows AI assistants to understand and use the returned information.

Next Steps

MCP Server

Dyngle can run as an MCP (Model Context Protocol) server, exposing operations as tools that AI assistants like Claude can execute.

What is MCP?

The Model Context Protocol (MCP) is a standardized protocol that allows AI assistants to discover and use external tools. When Dyngle runs as an MCP server, your configured operations become tools that AI assistants can call to perform tasks.

Starting the Server

Use the mcp command with the --config option to specify a configuration file:

dyngle --config ./.dyngle.mcp.yml mcp

This starts a server using the stdio (standard input/output) transport, which is ideal for integration with Claude Desktop and other AI assistants that support local MCP servers.

Recommended convention: Use .dyngle.mcp.yml as your MCP-specific configuration file to separate MCP-exposed operations from your regular workflow operations.

Filtering Operations

Use the --operations option to selectively expose only specific operations as MCP tools:

dyngle --config ./.dyngle.mcp.yml mcp --operations op1,op2,op3

This is useful when:

  • You have many operations but only want to expose a subset via MCP
  • You want different MCP server instances exposing different operations
  • You need to restrict which operations are available to AI assistants

Example: Expose only read-only operations:

dyngle --config ./.dyngle.mcp.yml mcp --operations get-status,list-items,read-log

The option accepts a comma-separated list of operation keys. Whitespace around commas is ignored. If any operation key is not found in the configuration, the command will fail with an error before starting the server.

If --operations is not specified, all public operations are exposed (the default behavior).

How Operations Become Tools

When the MCP server starts:

  1. Each public operation becomes an MCP tool
  2. Private operations are not exposed
  3. Tool input parameters depend on the operation's accepts: definition:
    • With accepts: The accept fields become the tool's input parameters
    • Without accepts: The tool has no input parameters

Tool Response Format

Tools return JSON responses:

Success:

{"result": <value>}

Where <value> is the operation's return value (if specified), or null if no return value.

Failure:

{"error": "<message>"}

Example: Operation with Interface

dyngle:
  operations:
    get-weather:
      description: Get current weather for a city
      accepts:
        city:
          type: string
      returns: weather-info
      steps:
        - curl -s "https://api.example.com/weather?city={{city}}" => weather-info

An AI assistant can call this tool with the interface parameters:

{
  "tool": "get-weather",
  "city": "San Francisco"
}

And receive:

{
  "result": {
    "temperature": 72,
    "conditions": "Sunny",
    "humidity": 65
  }
}

Example: Operation without Interface

dyngle:
  operations:
    run-backup:
      description: Run the nightly backup process
      returns: result
      steps:
        - /usr/local/bin/backup.sh => result

An AI assistant can call this tool with no parameters:

{
  "tool": "run-backup"
}

Configuring Claude Desktop

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

macOS Configuration

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

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

Important:

  • Use absolute paths for the configuration file
  • Restart Claude Desktop completely after editing (not just close the window)
  • Tools appear in Claude's "Search and tools" interface

Example with Project-Specific Config

For a project at /Users/alice/projects/myapp:

{
  "mcpServers": {
    "myapp": {
      "command": "dyngle",
      "args": ["--config", "/Users/alice/projects/myapp/.dyngle.mcp.yml", "mcp"]
    }
  }
}

If you don't specify a --config option, Dyngle follows the standard configuration file search order. See Configuration for details. However, it's recommended to always use --config with MCP to ensure the correct operations are exposed.

Design Considerations

Use Descriptions and Interfaces

Operations exposed via MCP should have clear descriptions and, where appropriate, explicit interface definitions:

dyngle:
  operations:
    deploy-app:
      description: Deploy the application to a specified environment
      accepts:
        environment:
          type: string
        version:
          type: string
      steps:
        - sub: build
        - aws s3 sync ./dist s3://{{environment}}-bucket/{{version}}/

The description helps AI assistants understand when to use the tool, and the interface makes the expected inputs explicit. See Operations for full interface syntax details.

Return Values and Interfaces

Operations used as tools should return meaningful values and use interfaces to define expected inputs:

dyngle:
  operations:
    check-status:
      description: Check deployment status
      accepts:
        service:
          type: string
      returns: status
      steps:
        - curl -s "https://api.example.com/status?service={{service}}" => status

This allows AI assistants to understand what inputs are needed and incorporate the result into their responses.

Private Operations for Secrets

Use private operations to protect sensitive operations:

dyngle:
  operations:
    get-credentials:
      access: private
      returns: creds
      steps:
        - aws secretsmanager get-secret-value --secret-id api-creds => creds
    
    make-api-call:
      description: Call the API with authentication
      steps:
        - sub: get-credentials
          receive: creds
        - curl -H "Authorization: {{creds}}" https://api.example.com/data

The get-credentials operation won't be exposed to AI assistants.

Example Configuration

Create a .dyngle.mcp.yml file with operations designed for AI assistant use:

dyngle:
  operations:
    # Development workflow
    run-tests:
      description: Run the test suite for a specific module
      accepts:
        module: { type: string }
      returns: test-results
      steps:
        - pytest {{module}} --json-report => results
        - results -> jq '.summary' => test-results
    
    # Information queries
    get-version:
      description: Get version from package.json
      accepts:
        package: { type: string }
      returns: version
      steps:
        - cat {{package}}/package.json => pkg
        - pkg -> jq -r '.version' => version
    
    # System operations
    check-service:
      description: Check the status of a specific application service
      accepts:
        service: { type: string }
      returns: status
      steps:
        - systemctl status {{service}} => output
        - echo "Service {{service}} is running" => status

See Use Cases for more examples of operations suitable for MCP integration.

Troubleshooting

Server Not Showing Up

  1. Check JSON syntax - Validate claude_desktop_config.json
  2. Verify Dyngle in PATH - Run which dyngle (macOS/Linux) or where dyngle (Windows)
  3. Use full path - Try the full path to the dyngle executable in the command field
  4. Restart Claude Desktop - Use Cmd+Q (macOS) or quit from system tray (Windows)

Checking Logs (macOS)

Claude Desktop writes logs to ~/Library/Logs/Claude/:

tail -n 20 -f ~/Library/Logs/Claude/mcp*.log

Next Steps

Configuration

Dyngle reads configuration from YAML files that define operations, expressions, constants, and other settings.

Configuration File Location

Dyngle searches for configuration files in the following order (first match wins):

  1. Command line option: --config parameter
  2. Environment variable: DYNGLE_CONFIG
  3. Current directory: .dyngle.yml
  4. Home directory: ~/.dyngle.yml

Examples

Using a specific config file:

dyngle --config /path/to/config.yml run my-operation

Using an environment variable:

export DYNGLE_CONFIG=/path/to/config.yml
dyngle run my-operation

Configuration Structure

A basic configuration file has this structure:

dyngle:
  operations:
    # Your operations here
    
  expressions:
    # Global expressions (optional)
    
  constants:
    # Global constants (optional)

Imports

Configuration files can import other configuration files, allowing you to organize your operations across multiple files and share common configurations.

Basic Import

Import other configuration files using the imports: key:

dyngle:
  imports:
    - ~/.dyngle.yml
    - ./common-operations.yml
  operations:
    # Operations defined here

Import Behavior

  • Imports are loaded in the order specified
  • Later imports override earlier ones in case of name conflicts
  • Operations and expressions in the main file override all imports
  • Imports are recursive - imported files can import other files

Use Cases

User-level configuration:

Create a ~/.dyngle.yml with common operations, then import it in project-specific configs:

# ~/.dyngle.yml
dyngle:
  operations:
    cleanup:
      - rm -rf .venv
      - rm -rf node_modules
# project/.dyngle.yml
dyngle:
  imports:
    - ~/.dyngle.yml
  operations:
    build:
      - npm install
      - npm run build

Shared team operations:

# team-shared.yml
dyngle:
  operations:
    deploy-staging:
      - sub: build
      - aws s3 sync ./dist s3://staging-bucket/

# developer's local config
dyngle:
  imports:
    - ./team-shared.yml
  operations:
    dev:
      - npm run dev

Next Steps

CLI Commands

Dyngle provides several commands for working with operations.

run

Execute a named operation from your configuration.

Syntax

dyngle run <operation-name> [optional arguments...]

Examples

Basic execution:

dyngle run hello

Pass command-line arguments that become available throughout the entire operation tree:

dyngle run greet Alice Bob

The run command is unique in supporting positional command-line arguments, as a convenience for common desktop operations. They can be accessed using the runtime.args context path.

dyngle:
  operations:
    greet:
      expressions:
        name: "get('runtime.args.0') or 'World'"
      steps:
        - echo "Hello {{name}}!"

With data from stdin:

echo "name: Alice" | dyngle run hello

With a specific config file:

dyngle --config /path/to/.dyngle.yml run deploy

Options

--display <mode>

Control step display behavior:

  • steps (default) - Show each step before executing
  • none - Suppress step display for cleaner output
dyngle run build --display none

When to Use Each Mode

Use steps mode when:

  • Debugging operations - See exactly what commands are being executed
  • Learning - Understand what's happening during execution
  • Development - Verify that template substitution is working correctly
  • Interactive use - Get visual confirmation of progress

Use none mode when:

  • Scripting - Cleaner output for parsing or processing
  • Production workflows - Reduce noise in logs
  • Return value focused - When you only care about the final result
  • Automated systems - CI/CD environments where step display is unnecessary

Examples

Development workflow with step display:

dyngle run test --display steps

Useful for seeing exactly what test commands are being run.

Production deployment with clean output:

dyngle run deploy --display none

Keeps deployment logs focused on command output without displaying each step.

Combining with return values:

When an operation has a return value, none mode is particularly useful:

dyngle:
  operations:
    get-version:
      returns: version
      steps:
        - cat package.json => pkg
        - pkg -> jq -r '.version' => version

With steps mode:

$ dyngle run get-version --display steps

Output:

$ cat package.json => pkg
$ pkg -> jq -r '.version' => version
1.2.3

With none mode:

$ dyngle run get-version --display none

Output:

1.2.3

Important Notes

  • The --display option controls whether step commands are shown before execution
  • This works independently of whether the operation has a returns: key (which controls stdout suppression)
  • stderr is always displayed regardless of the --display setting
  • If you don't specify --display, it defaults to steps mode

list-operations

List all available public operations with their descriptions.

Syntax

dyngle list-operations

Output Format

The command outputs YAML-formatted list of operations:

operations:
  build: Build the project for production
  test: Run the test suite
  deploy: Deploy to production

Behavior

  • Shows only public operations (not those with access: private)
  • Includes the description: attribute if present
  • Operations without descriptions show empty descriptions

See Operations for information about public vs private operations.

mcp

Start Dyngle as an MCP (Model Context Protocol) server, exposing operations as tools for AI assistants.

Syntax

dyngle mcp

This starts a server using the stdio (standard input/output) transport, which is suitable for integration with Claude Desktop and other AI assistants.

Configuration File

Specify a configuration file for the MCP server:

dyngle --config /path/to/.dyngle.yml mcp

See MCP Server for complete setup and usage information.

Global Options

These options work with any command:

--config <path>

Specify a configuration file:

dyngle --config ./custom.yml run hello

Next Steps

Use Cases

Real-world examples demonstrating how to use Dyngle for various tasks. These examples show practical patterns for building workflows, processing data, and integrating with AI assistants.

Development Workflows

Build Pipeline

Compose multiple build steps into a single operation:

dyngle:
  operations:
    install-deps:
      access: private
      - npm install
    
    compile:
      access: private
      - npm run build
    
    test:
      access: private
      - npm test
    
    build:
      description: Full build pipeline
      steps:
        - sub: install-deps
        - sub: compile
        - sub: test

Users run dyngle run build to execute the complete pipeline, while individual steps remain private.

Testing Module

Expose a test operation via MCP for AI assistants to run tests:

dyngle:
  operations:
    run-tests:
      description: Run the test suite for a specific module
      accepts:
        module: { type: string }
      returns: test-results
      steps:
        - pytest {{module}} --json-report => results
        - results -> jq '.summary' => test-results

An AI assistant can run tests and understand the results to help debug issues.

Continuous Integration

Build a complete CI/CD pipeline:

dyngle:
  operations:
    install-dependencies:
      access: private
      - npm install
    
    run-tests:
      access: private
      - npm test
    
    build-artifacts:
      access: private
      - npm run build
    
    upload-artifacts:
      access: private
      - aws s3 sync ./dist s3://my-bucket/
    
    ci-pipeline:
      description: Run full CI/CD pipeline
      steps:
        - sub: install-dependencies
        - sub: run-tests
        - sub: build-artifacts
        - sub: upload-artifacts

Version Management

Query and manage versions across a project:

dyngle:
  operations:
    get-version:
      description: Get version from package.json
      returns: version
      steps:
        - cat package.json -> jq -r '.version' => version
    
    tag-release:
      description: Tag a release with the current version
      steps:
        - sub: get-version
          receive: ver
        - git tag "v{{ver}}"
        - git push origin "v{{ver}}"

Data Processing

API Data Pipeline

Fetch and process data from an API:

dyngle:
  operations:
    get-user-emails:
      description: Extract email addresses from user API
      returns: emails
      steps:
        - curl -s "https://api.example.com/users" => users
        - users -> jq -r '.[].email' => emails

Log Analysis

Process log files to extract insights:

dyngle:
  operations:
    analyze-logs:
      description: Count errors in today's logs
      returns: summary
      steps:
        - curl -s "https://logs.example.com/today" => logs
        - logs -> grep "ERROR" => errors
        - errors -> wc -l => error-count
        - echo "Found {{error-count}} errors" => summary

Data Transformation

Transform JSON data through multiple stages:

dyngle:
  operations:
    fetch-data:
      access: private
      returns: raw
      steps:
        - curl -s "https://api.example.com/data" => raw
    
    transform-data:
      access: private
      accepts:
        input: { type: string }
      returns: output
      steps:
        - input -> jq '.items | map({id, name})' => output
    
    process-all:
      description: Fetch and transform data
      returns: final
      steps:
        - sub: fetch-data
          receive: data
        - sub: transform-data
          send: payload
          receive: final
      constants:
        payload:
          input: "{{data}}"

Operations and Deployment

Environment Deployment

Deploy to different environments with reusable setup:

dyngle:
  operations:
    setup-env:
      access: private
      steps:
        - echo "Setting up environment..."
        - export NODE_ENV=production
    
    deploy-frontend:
      description: Deploy frontend application
      steps:
        - sub: setup-env
        - npm run deploy:frontend
    
    deploy-backend:
      description: Deploy backend services
      steps:
        - sub: setup-env
        - npm run deploy:backend

Multi-Environment Deployment

Deploy with environment-specific configuration:

dyngle:
  operations:
    deploy:
      description: Deploy application to specified environment
      accepts:
        environment:
          type: string
        version:
          type: string
      steps:
        - sub: build
        - aws s3 sync ./dist s3://{{environment}}-bucket/{{version}}/
        - echo "Deployed v{{environment}} to {{environment}}"
    
    build:
      access: private
      - npm run build

Service Health Check

Check service status and return structured information:

dyngle:
  operations:
    check-service:
      description: Check the status of a specific application service
      accepts:
        service: { type: string }
      returns: status
      steps:
        - systemctl status {{service}} => output
        - echo "Service {{service}} is running" => status

Perfect for MCP integration - AI assistants can check service health.

Secret Management

Secure Credential Access

Protect operations that handle secrets:

dyngle:
  operations:
    get-api-token:
      access: private
      returns: token
      steps:
        - aws secretsmanager get-secret-value --secret-id api-token => secret
        - secret -> jq -r '.SecretString' => token
    
    call-api:
      description: Make authenticated API call
      steps:
        - sub: get-api-token
          receive: token
        - curl -H "Authorization: Bearer {{token}}" https://api.example.com/data

The get-api-token operation is private so it can't be run directly or exposed via MCP, preventing accidental token exposure.

Database Credentials

Safely manage database credentials:

dyngle:
  operations:
    get-db-credentials:
      access: private
      returns: creds
      steps:
        - aws secretsmanager get-secret-value --secret-id db-creds => creds
    
    backup-database:
      description: Create database backup
      steps:
        - sub: get-db-credentials
          receive: creds
        - pg_dump -h {{creds.host}} -U {{creds.user}} > backup.sql

AI Assistant Integration (MCP)

Code Analysis

Expose operations that help AI assistants understand codebases:

dyngle:
  operations:
    get-package-info:
      description: Get package information from package.json
      returns: info
      steps:
        - cat package.json => pkg
        - pkg -> jq '{name, version, description, dependencies}' => info
    
    get-dependencies:
      description: List all project dependencies
      returns: deps
      steps:
        - cat package.json -> jq '.dependencies | keys' => deps

Project Information

Provide project context to AI assistants:

dyngle:
  operations:
    project-summary:
      description: Get a summary of the project structure
      returns: summary
      expressions:
        summary:
          name: "get('pkg')['name']"
          version: "get('pkg')['version']"
          files: "get('file-count')"
      steps:
        - cat package.json => pkg
        - find . -type f | wc -l => file-count

Development Environment Status

Help AI assistants understand the current development state:

dyngle:
  operations:
    dev-status:
      description: Check development environment status
      returns: status
      expressions:
        status:
          git-branch: "get('branch')"
          uncommitted-changes: "get('changed-files')"
          node-version: "get('node-ver')"
      steps:
        - git branch --show-current => branch
        - git status --short | wc -l => changed-files
        - node --version => node-ver

Content Management

Document Processing

Process markdown or text files:

dyngle:
  operations:
    count-words:
      description: Count words in a markdown file
      accepts:
        file: { type: string }
      returns: count
      steps:
        - cat {{file}} => content
        - content -> wc -w => count
    
    extract-headings:
      description: Extract headings from markdown
      accepts:
        file: { type: string }
      returns: headings
      steps:
        - cat {{file}} => content
        - content -> grep "^#" => headings

Site Building

Build and deploy static sites:

dyngle:
  operations:
    build-site:
      access: private
      - hugo build
    
    deploy-site:
      description: Build and deploy the site
      steps:
        - sub: build-site
        - aws s3 sync ./public s3://my-site-bucket/
        - aws cloudfront create-invalidation --distribution-id DIST123 --paths "/*"

System Administration

Backup Operations

Automated backup with composition:

dyngle:
  operations:
    backup-files:
      access: private
      - tar -czf backup-$(date +%Y%m%d).tar.gz ./data
    
    upload-backup:
      access: private
      - aws s3 cp backup-*.tar.gz s3://backup-bucket/
    
    cleanup-old:
      access: private
      - find ./backup-*.tar.gz -mtime +7 -delete
    
    backup:
      description: Complete backup workflow
      steps:
        - sub: backup-files
        - sub: upload-backup
        - sub: cleanup-old

Service Management

Manage application services:

dyngle:
  operations:
    restart-app:
      description: Restart application services
      steps:
        - systemctl stop myapp
        - sleep 5
        - systemctl start myapp
        - systemctl status myapp

Best Practices Demonstrated

Composition Over Complexity

Break complex operations into smaller, focused operations:

Good:

dyngle:
  operations:
    validate:
      access: private
      - python validate.py
    
    transform:
      access: private
      - python transform.py
    
    upload:
      access: private
      - aws s3 sync ./output s3://bucket/
    
    process:
      description: Complete processing workflow
      steps:
        - sub: validate
        - sub: transform
        - sub: upload

Avoid:

dyngle:
  operations:
    process:
      - python validate.py
      - python transform.py
      - aws s3 sync ./output s3://bucket/

While the "avoid" version works, the composed version is more testable, maintainable, and allows reuse of individual steps.

Clear Interfaces

Define explicit interfaces for operations:

dyngle:
  operations:
    deploy-service:
      description: Deploy a service to an environment
      accepts:
        service-name: { type: string }
        environment: { type: string }
        version: { type: string }
      steps:
        - echo "Deploying {{service-name}} v{{version}} to {{environment}}"

This serves as documentation and provides validation.

Return Meaningful Data

Operations exposed via MCP should return structured data:

dyngle:
  operations:
    analyze-project:
      description: Analyze project health
      returns: analysis
      expressions:
        analysis:
          test-coverage: "get('coverage')"
          lint-errors: "get('errors')"
          build-status: "get('status')"
      steps:
        - pytest --cov --json-report => cov-report
        - cov-report -> jq '.coverage' => coverage
        - eslint . --format json => lint-report
        - lint-report -> jq '.[] | .errorCount' => errors
        - echo "healthy" => status

Next Steps