Workflows


Introduction

A workflow is a directed acyclic graph (DAG) of execution steps that combine Web2 services, blockchain interactions, and AI logic into a single verifiable process. Each step is isolated, cryptographically attested, and connected through explicit input/output bindings.

Structure of a Workflow

A workflow is divided logical checkpoints. This segmentation makes it easier for developers to design, audit, and verify workflows while ensuring all mandatory protocol components are in place.

{
  //Basic info for the execution content
  "chain_id": <chain id>,                       // Chain ID of the target chain
  "sender": "<address>",                        // Wallet address initiating execution
  "delegate": "<delegate-address>",             // Optional delegated account
  "attestor": "<attestor-url>",                 // IPFS or signed attestor config
  
  // Details of the Target contract
  "target": {
    "contract": "{{ENV.TARGET_CONTRACT}}",      // Address of the target contract (passed as env)
    "function": "<function signature>",         // Function signature to call on the target contract
    "authData_result": "${construct-property-analysis-evm.result}", // Referencing the result of the EVM CONSTRUCT STEP
    "parameters": [] // If there are any extra param other than the AuthData tuple can be handled here
  },
  "sponsor_execution_fee": true, // If you want to sponsor the execution fee for the user
  "value": "0", // If the target function signature is payable
  "intent": {
    "id": "{{TRANSACTION_INTENT_ID}}",                // Unique identifier for intent
    "signature": "{{USER_SIGNATURE}}",                // User signature over intent
    "deadline": "{{TRANSACTION_INTENT_DEADLINE}}"     // Expiry timestamp
  },
  
  // RPC and Bundler config
  "rpc_url": "${_SECRETS.rpcSepoliaURL}",
  "bundler_url": "https://api.pimlico.io/v2/sepolia/rpc?apikey=${_SECRETS.pimlico-apikey}",
  "paymaster_url": "https://api.pimlico.io/v2/sepolia/rpc?apikey=${_SECRETS.pimlico-apikey}",
  "gas_limit": "100000",
  "max_fee_per_gas": "20000000000",
  "max_priority_fee_per_gas": "2000000000",
  
  // Workflow steps
  "workflow": {
    "name": "<workflow-name>",
    "version": "1.0.0",
    "steps": [
      {
        "name": "<step-name>",
        "type": "<step-type>",                  // HTTP, EVM_READ, EVM_ENCODER, BUNDLE
        "image": "<executor-image>",            // Containerized executor image
        "attestor": "<attestor-url>",           // Attestor configuration
        "next": "<next-step>",                  // Optional link to next step
        "config": { },                          // Step-specific configuration
        "inputs": { },                          // Data from user or previous step
        "outputs": [ ]                          // Data exported for next steps
      }
    ]
  }
}

A workflow definition contains 4 logical checkpoints:

1️⃣ Basic Info

2️⃣ Target Contract Details

3️⃣ RPC and Bundler Config

4️⃣ Workflow Steps

1. Basic Info

Defines the the context for execution.

{
  "chain_id": <chain id>,                       // Chain ID of the target chain
  "sender": "<address>",                        // Wallet address initiating execution
  "delegate": "<delegate-address>",             // Optional delegated account
  "attestor": "<attestor-url>",                 // IPFS or signed attestor config
}

Field-by-field explanation:

  • chain_id

    • Identifies the blockchain network where the transaction/workflow will execute.

    • Examples:

      • 1 → Ethereum mainnet

      • 11155111 → Ethereum Sepolia testnet

    • Prevents replay attacks across chains (so the intent is valid only on the specified network).

  • sender

    • The EOA (Externally Owned Account) or contract account that originates the workflow.

    • This is the primary identity responsible for initiating execution.

    • Typically signs the intent to prove authorization.

  • delegate (optional)

    • An alternate account that executes on behalf of the sender.

    • Useful for:

      • Delegated accounts (EIP-7702 / Account Abstraction patterns).

      • Letting a smart contract wallet or sponsor pay fees and submit the transaction.

    • If unset, the sender executes directly.

  • attestor

    • Points to the attestation policy that validates this execution.

    • Could be:

      • An IPFS hash storing attestor configuration.

      • A container image reference (image://…) that encapsulates attestation logic.

    • Ensures that every workflow step is verifiable and comes from a trusted source.


2. Target Contract Details

Defines the the context for execution.

{
  //basic info above (chain_id, sender, etc.)

  "target": {
    "contract": "{{ENV.TARGET_CONTRACT}}",      // Address of the target contract (passed as env)
    "function": "<function signature>",         // Function signature to call on the target contract
    "authData_result": "${construct-property-analysis-evm.result}", 
                                                // Reference to the encoded data produced 
                                                // by a previous EVM_CONSTRUCT step
    "parameters": []                            // Any additional parameters beyond AuthData tuple
  },
  "sponsor_execution_fee": true,                // If true, execution gas is covered by sponsor
  "value": "0",                                 // ETH or native token value sent along (if payable)
  "intent": {
    "id": "{{TRANSACTION_INTENT_ID}}",          // Unique identifier for this execution intent
    "signature": "{{USER_SIGNATURE}}",          // User’s cryptographic signature authorizing execution
    "deadline": "{{TRANSACTION_INTENT_DEADLINE}}" 
                                                // Expiry timestamp after which intent is invalid
  }
}

Field-by-field breakdown

  • target

    • contract

      • The on-chain smart contract address you want to call.

        • Typically passed as an environment variable so it can change across deployments (testnet vs. mainnet).

    • function

      • The ABI function signature (e.g., submitPropertyAnalysis((uint256,string))).

        • Defines exactly what function of the contract will be executed.

    • authData_result

      • Refers to output from a previous workflow step — specifically an EVM ENCODER/CONSTRUCT step that builds the calldata (like ABI encoding).

        • Keeps the pipeline composable: workflow steps generate data, and this field consumes it.

    • parameters

      • Holds any extra arguments that the contract function might need, besides the primary AuthData.

        • Empty array [] if not needed.


  • sponsor_execution_fee

    • Boolean flag:

      • true → A sponsor (e.g., paymaster) covers the gas fees for this execution.

        • false → The user pays fees directly.

    • Implements gasless transactions or meta-transactions.


  • value

    • The amount of native token (e.g., ETH, MATIC) sent along with the contract call.

    • 0 if the function is not payable.

    • Non-zero when the function requires value transfer.


  • intent

    • Encapsulates user authorization for the execution.

      • id → A globally unique identifier for this execution request. Prevents replay and duplication.

        • signature → The cryptographic signature from the sender proving they approved this intent.

        • deadline → Expiration timestamp. After this point, the intent can no longer be executed, even if signed


3. RPC and Bundler Config

Defines the the context for execution.

{
  //basic info above (chain_id, sender, etc.)
  //target contract details

  "rpc_url": "${_SECRETS.rpcSepoliaURL}",                   // RPC endpoint for chain interaction
  "bundler_url": "https://api.pimlico.io/v2/sepolia/rpc?apikey=${_SECRETS.pimlico-apikey}", 
                                                            // Bundler service for Account Abstraction txs
  "paymaster_url": "https://api.pimlico.io/v2/sepolia/rpc?apikey=${_SECRETS.pimlico-apikey}", 
                                                            // Paymaster service (for sponsored gas)
  "gas_limit": "100000",                                    // Upper bound on gas consumption for tx
  "max_fee_per_gas": "20000000000",                         // Absolute max gas fee user is willing to pay
  "max_priority_fee_per_gas": "2000000000"                  // Miner/validator tip to prioritize tx
}

Field-by-field explanation

  • rpc_url

    • The Ethereum JSON-RPC endpoint used to interact with the target chain (Sepolia in this case).

    • Provided via a secret (_SECRETS.rpcSepoliaURL) for security.

    • Required for submitting transactions and reading chain state.

  • bundler_url

    • The ERC-4337 bundler service endpoint.

    • A bundler collects multiple user operations (UserOps) and submits them as transactions.

    • This enables Account Abstraction workflows where EOAs don’t directly broadcast transactions.

  • paymaster_url

    • Endpoint for a paymaster service, which can sponsor or subsidize gas fees.

    • Lets dApps or protocols offer gasless user experiences.

    • Works in conjunction with the bundler.

  • gas_limit

    • The maximum amount of gas this transaction is allowed to consume.

    • Prevents runaway execution and ensures cost predictability.

  • max_fee_per_gas

    • Defines the absolute maximum fee per unit of gas the user is willing to pay.

    • Denominated in wei (here: 20 gwei).

    • Protects the user from sudden fee spikes.

  • max_priority_fee_per_gas

    • The tip paid to validators/miners on top of the base fee.

    • Incentivizes faster inclusion of the transaction.

    • Here: 2 gwei.

2. Workflow Steps (Business Logic)

The core business logic, expressed as a DAG of steps. Each step is executed by an executor and verified by an attestor. We categorize workflow steps into five major types:

2.1. HTTP Request Steps

What it is: Perform Web2 API requests (GET, POST). When to use: Fetch off-chain data (prices, telemetry, compliance feeds) or write to external APIs/databases.

Example – GET:

{
  "name": "fetch-telemetry",
  "image": "ghcr.io/krnl-labs/executor-http@sha256:7ea7eae002f173aab994444e9e0863f8a8fd8255c7ed3a234218e66c3e1f3c60",
  "attestor": "https://public.mypinata.cloud/ipfs/bafybeid3msoebov6o54rtvjtrdwv7fg6tkeye2skrxh6jis25zok6pavzi",
  "next": "write-db",
  "config": {},
  "inputs": {
    "url": "https://doc.platform.lat/metacube/telemetry",
    "method": "GET"
  },
  "outputs": [
    {
      "name": "cpu_usage_percent",
      "value": "response.body.cpu_usage_percent",
      "export": true
    }
  ]
}

Field-by-field Explanation

  • name: Human-readable label (fetch-telemetry).

  • image: Executor container image – this one is a specific digest of the HTTP executor.

  • attestor: Cryptographic attestation source (IPFS file/image link with verification metadata).

  • next: Defines the step to run after this (write-db).

  • config: Reserved for step-specific tuning (empty here).

  • inputs:

    • url: Endpoint to fetch telemetry data from.

    • method: "GET" since it’s just retrieving.

  • outputs:

    • Maps a value from the HTTP response → workflow variable.

    • Here: "response.body.cpu_usage_percent" is extracted and exported as cpu_usage_percent for downstream steps.

Example – POST:

{
  "name": "write-db",
  "image": "ghcr.io/krnl-labs/executor-http@sha256:...",
  "attestor": "ipfs://.../attestor.json",
  "next": "construct-evm",
  "config": {},
  "inputs": {
    "url": "https://doc.platform.lat/zetaabc/telemetry",
    "method": "POST",
    "headers": {
      "Content-Type": "application/json"
    },
    "body": {
      "cpu": "80",
      "mem": "85"
    }
  },
  "outputs": [
    {
      "name": "status",
      "value": "response.body.status",
      "export": true
    }
  ]
}

Field-by-field Explanation

  • name: Step identifier (write-db).

  • image: Again the HTTP executor container (different digest).

  • attestor: IPFS link to its attestation metadata.

  • next: Next step is construct-evm (likely assembling a transaction).

  • inputs:

    • url: Target API for database write.

    • method: "POST".

    • headers: Standard JSON content type.

    • body: JSON payload with telemetry (cpu, mem).

  • outputs:

    • Extracts "response.body.status" and exports as status.


2.2. EVM Read Steps

What it is: Query smart contract state via read-only calls. When to use: Fetch balances, interest rates, prices, or other on-chain data without modifying state.

Example:

{
  "name": "EVM-data-fetcher",
  "image": "ghcr.io/krnl-labs/executor-evm-read@sha256:9c15f3e004352f1062a22b8bf7d7fa80498449b9407bc6efc107dbdd7acce5a4",
  "attestor": "https://public.mypinata.cloud/ipfs/bafybeid3msoebov6o54rtvjtrdwv7fg6tkeye2skrxh6jis25zok6pavzi",
  "next": "construct-evm",
  "config": {
    "function_signature": "balanceOf(address)",
    "input_parameters": [
      {
        "name": "account",
        "type": "address"
      }
    ],
    "output_parameters": [
      {
        "name": "balance",
        "type": "uint256"
      }
    ]
  },
  "inputs": {
    "value": {
      "account": "0x907089fC3966f52dB446345451Ad9aE3B164D94c"
    },
    "url": "https://lb.drpc.org/optimism-sepolia/",
    "chainid": 11155420,
    "contractAddress": "0xB9467B24117FD79D56F396ADC3cCDB695D905ae4"
  },
  "outputs": [
    {
      "name": "balance",
      "value": "response.0",
      "export": true
    }
  ]
}

Field-by-field Explanation

  • name

    • Step identifier = "EVM-data-fetcher".

    • Labels this as the on-chain query node.

  • image

    • Executor container: EVM Read executor.

    • Specialized runtime that can encode calls, query RPC endpoints, and decode results.

  • attestor

    • IPFS link to the attestation config.

    • Ensures this step’s execution is cryptographically verifiable.

  • next

    • Workflow link → next step is "construct-evm" (likely assembling a transaction).


config (call definition)

  • function_signature: "balanceOf(address)"

    • The ABI function to call on the smart contract.

    • Standard ERC-20 balance check.

  • input_parameters:

    • Defines the inputs schema — here:

      • account (type: address).

  • output_parameters:

    • Defines the expected return values — here:

      • balance (type: uint256).


inputs (runtime values)

  • value:

    • Passes the actual arguments for the call.

    • "account" = 0x9070...D94c.

  • url:

    • RPC endpoint to query (Optimism Sepolia via DRPC load balancer).

  • chainid:

    • Numeric chain ID = 11155420 (Optimism Sepolia).

  • contractAddress:

    • The contract instance being queried (0xB9467...5ae4).


outputs (exported result)

  • Defines what values are extracted from the RPC response.

  • value: "response.0"

    • Means: take the first returned value from the call (the balance).

  • name: "balance"

    • Store it under this variable name.

  • export: true

    • Makes it available to subsequent workflow steps (e.g., construct-evm).


2.3. EVM Construct

Encodes raw workflow data into ABI-compliant calldata. This represents the response expected by your target smart contract.

  • Takes workflow results as inputs.

  • Outputs a bytes payload ready for target contract consumption.

Example:



// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

import "./TargetBase.sol"; // Import base contract that provides AuthData struct + requireAuth modifier

contract TelemetryContract is TargetBase {

    // Expected response struct from the KRNL Node 
    // Fields must be in alphabetic order (to match encoding/decoding consistency)
    struct ExpectedResponse {
        string cpu_usage_percent;   // Telemetry: CPU usage
        string mem_usage_percent;   // Telemetry: Memory usage
    }
    
    // Target Function Signature (called by workflows after KRNL attestation)
    function submitTelemetry(AuthData calldata authData) 
        external 
        requireAuth(authData) // Ensures KRNL attestation & signature are valid
    {
        // Decode the KRNL response (AuthData.result is a raw ABI-encoded payload)
        ExpectedResponse memory krnlResponse = abi.decode(
            authData.result, // Workflow-encoded calldata
            (ExpectedResponse) // Decode into ExpectedResponse struct
        );
        
        // TODO: Application-specific logic goes here
        // e.g. store telemetry, trigger alerts, reward nodes, etc.
        ....
        ....
    }
}


{
  "name": "construct-evm", // Step name: encode telemetry into ABI-compliant calldata
  "image": "ghcr.io/krnl-labs/executor-encoder-evm@sha256:...", // Executor image for EVM ABI encoding
  "attestor": "https://public.mypinata.cloud/ipfs/.../attestor.json", // Attestor verifying correct encoding
  "next": "prepare-authdata", // Next system step in pipeline (adds cryptographic proofs)

  "config": {
    "parameters": [
      {
        "name": "telemetry",   // Top-level tuple name
        "type": "tuple",       // Encoded as Solidity struct
        "components": [        // Must align with ExpectedResponse struct in Solidity
          {
            "name": "cpuUsagePercent", // Matches struct field
            "type": "string"
          },
          {
            "name": "memUsagePercent", // Matches struct field
            "type": "string"
          }
        ]
      }
    ]
  },

  "inputs": {
    "value": {
      "telemetry": {
        // Internal references pull results from earlier workflow steps
        "cpuUsagePercent": "${workflow_step_1.result}", 
        "memUsagePercent": "${workflow_step_2.result}"
      }
    }
  },

  "outputs": [
    {
      "name": "result",   // Encoded ABI payload
      "value": "result",  // Standard output from encoder
      "export": true      // Make available to next step (prepare-authdata)
    }
  ]
}

Best Practices

  • Keep authData_result separate from extra parameters

    • Protocol-level info (nonce, expiry, signature) → authData_result.

    • dApp-specific parameters → parameters array.

  • Explicit function signature

    • Include full ABI signature in target.function for clarity.

    • Example:

      "function": "purchaseTokens((uint256,uint256,bytes32,(bytes32,bytes,bytes)[],bytes,bool,bytes),uint256,address)"
  • Use environment variables

    • Sensitive info like contract addresses, RPC URLs → ${ENV.*} or ${_SECRETS.*}.

    • Example:

      "contract": "${ENV.TARGET_CONTRACT}"
  • Include replay & intent protection

    • Always provide intent.id, intent.signature, intent.deadline.

    • Prevents replay attacks across chains or repeated execution.

  • Explicit data binding

    • Outputs from previous steps → referenced using ${step-name.output}.

    • Avoid implicit assumptions; makes workflows auditable.

  • Validation in DSL

    • Simple type checks (string, uint256, address) in the workflow before contract call.

    • Helps catch issues off-chain before execution.

What's next?

KRNL Node

Last updated