add-task

star 173

TRIGGER when user asks to add a workflow step, agent step, agentic task, task endpoint, or workflow phase. "Agent step" and "workflow step" are interchangeable - a task is a single step of a workflow / agent / agentic workflow, regardless of whether the surrounding workflow calls an LLM. Tasks are handlers that read/write shared state via workflow.Flow. Affects intermediate.go, *api/client.go, mock.go, manifest.yaml.

microbus-io By microbus-io schedule Updated 6/13/2026

name: add-task description: TRIGGER when user asks to add a workflow step, agent step, agentic task, task endpoint, or workflow phase. "Agent step" and "workflow step" are interchangeable - a task is a single step of a workflow / agent, whether or not the surrounding workflow calls an LLM.

CRITICAL: Do NOT explore or analyze other microservices unless explicitly instructed to do so. The instructions in this skill are self-contained to this microservice.

CRITICAL: A task is declared as a define.Task var in <name>api/definition.go and implemented as a handler in service.go. Add the declaration and run cmd/genservice.

CRITICAL: Keep the // MARKER: Name comment on the define.Task var and on its In/Out structs.

IMPORTANT: Read .claude/rules/workflows.txt for workflow and task conventions before proceeding.

Workflow

Copy this checklist and track your progress:

Creating or modifying a task endpoint:
- [ ] Step 1: Read local CLAUDE.md file
- [ ] Step 2: Determine the signature
- [ ] Step 3: Determine the route
- [ ] Step 4: Determine a description
- [ ] Step 5: Determine the required claims
- [ ] Step 6: Define complex types
- [ ] Step 7: Declare the task in definition.go
- [ ] Step 8: Implement the logic in service.go
- [ ] Step 9: Generate the boilerplate
- [ ] Step 10: Test the task
- [ ] Step 11: Housekeeping

Step 1: Read Local CLAUDE.md File

Read the local CLAUDE.md file in the microservice's directory. It contains microservice-specific instructions that should take precedence over global instructions.

Ensure the local CLAUDE.md advertises that this microservice implements agentic workflows. The Agent Instructions block holds one short paragraph per instruction so multiple instructions (workflows, SQL, auth) can coexist as separate paragraphs. The paragraph to add is:

This microservice implements agentic workflows. See `.claude/rules/workflows.txt` for the conventions.

How to apply:

  • If the file does not exist, create it with the hostname as an H1 heading, then add an ## Agent Instructions section containing the paragraph above.
  • If the file exists and already has an ## Agent Instructions section, append the paragraph (separated by a blank line) after the existing instructions; skip if a workflows-related paragraph is already present.
  • If the file exists but has no ## Agent Instructions section, insert one as the first section after the H1 hostname heading and add the paragraph.

Step 2: Determine the Signature

Determine the Go signature of the task endpoint. A task always receives ctx context.Context and flow *workflow.Flow as its first two arguments, followed by state fields it reads as input. It returns state fields it writes as output, plus err error.

func MyTask(ctx context.Context, flow *workflow.Flow, input1 string, input2 float64) (output1 bool, err error)

Constraints:

  • The first argument must be ctx context.Context
  • The second argument must be flow *workflow.Flow
  • The function must return an err error
  • All input arguments (after flow) represent state fields read from the workflow state
  • All output arguments (except err) represent state fields written to the workflow state
  • To read and modify the same state field, use the Out suffix on the return value - the generator strips Out to map back to the same state key (e.g. input counter int and output counterOut int both map to state key "counter")
  • Complex types (structs) are allowed by value or by reference
  • All arguments must be serializable into JSON
  • Arguments must not be named t or svc
  • Argument names must start with a lowercase letter
  • The function name must start with an uppercase letter

Naming for fan-in: argument names carry no execution semantics. A fan-in field's reducer is set explicitly at graph-build time with graph.SetReducer(field, reducer); without one, the default is Replace (last write wins). Pick names that describe what the field is (e.g. failures, messages, score), not the merge strategy. Tasks writing to a reducer-managed field must produce only the delta for this branch, not the full accumulated value - otherwise fan-in produces duplicates. For example, a VerifyEmployment task running once per employer with graph.SetReducer("failures", workflow.ReducerAdd) should return failuresOut: 0 or 1 (its own count), not the running total.

Prefer typed input/output arguments over flow.Get / flow.Set. Inputs and outputs are auto-bound to state by name; the signature is the task's state contract, mocks get typed handlers, and a reader sees what the task reads and produces without scanning the body. Reserve flow.Get / flow.Set for keys whose names are dynamic or for internal types not in the API package. See the Best Practices section of .claude/rules/workflows.txt for the rationale.

forEach branches see auto-injected per-element fields. When the task runs as the target of AddTransitionForEach(..., "items", "item"), the branch's state contains item (the element), itemIndex (0-based position), and itemCount (cohort size). Take any of them as a typed argument by name - no lookup code needed.

Step 3: Determine the Route

The route of the task endpoint is resolved relative to the hostname of the microservice. Tasks use the dedicated port :428 to prevent external access. Use the name of the task in kebab-case as its route, e.g. :428/my-task.

Step 4: Determine a Description

Describe the task starting with its name, in Go doc style: MyTask does X. This becomes the godoc comment on the define.Task var.

Describe what the task does and the effect it produces, not who or what is expected to invoke it. "Computes the credit score from the applicant's history" is good; "called by the credit-review workflow" or "used by the LLM as a tool" is not.

Step 5: Determine the Required Claims

Determine if the task endpoint should be restricted to authorized actors only. Compose a boolean expression over the JWT claims associated with the request that if not met will cause the request to be denied. For example: roles.manager && level>2. Leave empty if the task should be accessible by all.

Step 6: Define Complex Types

Identify the struct types in the signature. Define these complex types in the myserviceapi directory. Skip this step if there are no complex types.

Place each definition in a separate file named after the type, e.g. myserviceapi/mystruct.go.

If the complex type is owned by this microservice, define its struct explicitly. Include json tags with camelCase names and the omitzero option, and a short jsonschema description tag on each field.

package myserviceapi

// MyStruct is X.
type MyStruct struct {
    FooField string `json:"fooField,omitzero" jsonschema_description:"FooField is X"`
    BarField int    `json:"barField,omitzero" jsonschema_description:"BarField is X"`
}

If the complex type is owned by another microservice, define an alias to it instead.

package myserviceapi

import (
    "github.com/path/to/thirdparty"
)

// ThirdPartyStruct is X.
type ThirdPartyStruct = thirdparty.ThirdPartyStruct

Step 7: Declare the Task in definition.go

Append the define.Task var and its In/Out structs to myserviceapi/definition.go. Tasks always use the POST method.

// MyTask does X.
var MyTask = define.Task{ // MARKER: MyTask
    Host: Hostname, Method: "POST", Route: ":428/my-task",
    In: MyTaskIn{}, Out: MyTaskOut{},
}

// MyTaskIn are the input arguments of MyTask.
type MyTaskIn struct { // MARKER: MyTask
    Input1 string  `json:"input1,omitzero"`
    Input2 float64 `json:"input2,omitzero"`
}

// MyTaskOut are the output arguments of MyTask.
type MyTaskOut struct { // MARKER: MyTask
    Output1 bool `json:"output1,omitzero"`
}
  • Host is always Hostname. Method is always POST. Route comes from Step 3
  • The In struct holds the input arguments excluding ctx and flow; the Out struct holds the output arguments excluding err
  • For an output field with the Out suffix, strip the suffix from the JSON tag so it maps to the same state key as the input (e.g. CounterOut int with json:"counter,omitzero")
  • If an In/Out field's type comes from another package (e.g. a time.Time field needs "time"), add that import to definition.go
  • Add the gating fields only when needed:
    • RequiredClaims: "roles.manager && level>2" for the claims from Step 5 (omit when open)
    • TimeBudget: 5 * time.Minute if the task has a known runtime ceiling shorter than the foreman default (2m, hard ceiling 15m); add the "time" import. For work that does not fit within 15m, use the Interrupt-and-Resume or Polling-with-Retry patterns from .claude/rules/workflows.txt

Step 8: Implement the Logic in service.go

Implement the task in service.go. Complex types refer to their definition in myserviceapi, even if owned by a third-party.

The task receives state fields as input arguments and returns state fields as output. It also has access to flow for control operations (flow.Goto(), flow.Interrupt(), flow.Subgraph(), flow.Retry(), flow.Sleep()) and for field-based state access (flow.GetString(), flow.Set()) when needed. Interrupt and Subgraph both park the step and return (data, yield, err); the task must return nil when yield is true and may read data / branch on err once it resolves (see "Subgraphs and Interrupts" in .claude/rules/workflows.txt).

// MyTask does X.
func (svc *Service) MyTask(ctx context.Context, flow *workflow.Flow, input1 string, input2 float64) (output1 bool, err error) { // MARKER: MyTask
    // Implement logic here...
    return
}

To invoke another microservice's task as an isolated subtask from inside this task body, use the generated Subflow client of that microservice (only the explicit inputs cross in, only the explicit outputs cross back). Do not use its Executor from a task body - the Executor is test-only.

output1, yield, err := otherapi.NewSubflow(flow).OtherTask(ctx, input1, input2)
if yield || err != nil {
    return err
}
// use output1

Idempotency. Tasks may be replayed: flow.Retry, worker-death recovery, and Subgraph re-entry all re-run the task body from the top. A task that fires an external side effect (charge a card, send an email, write to a non-transactional store) must carry its own dedupe key or check first whether the effect has already happened. The framework does not deduplicate side effects for you. Pure computation over state needs no special treatment.

State hygiene. If this task consumes large intermediates (LLM response, parsed payload, raw API body, image bytes) that downstream tasks do not need, drop them before returning. Three primitives compose for any cleanup pattern:

  • flow.Delete(names...) - drop the listed fields.
  • flow.Clear() - drop every field; typical in a task that is about to build a fresh subgraph input from scratch.
  • flow.Transform("newKey", "oldKey", ...) - clear all state, then re-introduce the listed fields under new names. Doubles as a "keep these" primitive when called with ("name", "name") pairs.

Each records JSON null in the step's changes for dropped fields, so the cleanup is preserved in the audit trail; downstream merged state is absent the field (Replace reducer) or sees no contribution (Add/Append/Union/Merge/And/Or/Concat short-circuit to their identity when a branch's value is JSON null).

Step 9: Generate the Boilerplate

From the microservice's directory, run the generator. It regenerates myserviceapi/client.go (the Executor and Subflow methods), intermediate.go (the marshaler, ToDo entry, and subscription), mock.go, mock_test.go, and manifest.yaml from the updated definition.go.

go run github.com/microbus-io/fabric/cmd/genservice .

Then, from the project root, bring the module's dependencies up to date and verify the microservice compiles:

go mod tidy
go vet ./...

Run go mod tidy first: a task that introduces a new import (a downstream client, or the foreman for subflows and workflow tests) can pull transitive dependencies that are not yet in go.sum, which makes go vet fail with missing go.sum entry until the module is tidied.

Step 10: Test the Task

Append the integration test to service_test.go. The test calls the task endpoint directly via the generated Executor without needing the foreman.

func TestMyService_MyTask(t *testing.T) { // MARKER: MyTask
    t.Parallel()
    ctx := t.Context()
    _ = ctx

    // Initialize the microservice under test
    svc := NewService()

    // Initialize the testers
    tester := connector.New("tester.client")
    exec := myserviceapi.NewExecutor(tester)
    _ = exec

    // Run the testing app
    app := application.New()
    app.Add(
        // HINT: Add microservices or mocks required for this test
        svc,
        tester,
    )
    app.RunInTest(t)

    /*
        HINT: Use the following pattern for each test case.
        Use WithOutputFlow to also verify control signals (Goto, Retry, Interrupt, Sleep) if applicable.

        t.Run("test_case_name", func(t *testing.T) {
            assert := testarossa.For(t)

            var outFlow workflow.Flow
            output1, err := exec.WithOutputFlow(&outFlow).MyTask(ctx, input1, input2)
            if assert.NoError(err) {
                assert.Expect(output1, expectedResult1)
                _, interrupted := outFlow.InterruptRequested()
                assert.Expect(interrupted, true)
            }
        })
    */
}

Skip the remainder of this step if instructed to be "quick" or to skip tests.

Insert test cases at the bottom of the integration test function using the recommended pattern. Do not remove the HINT comments.

t.Run("test_case_name", func(t *testing.T) {
    assert := testarossa.For(t)

    output1, err := exec.MyTask(ctx, input1, input2)
    if assert.NoError(err) {
        assert.Expect(output1, expectedResult1)
    }
})

Step 11: Housekeeping

Follow the housekeeping skill.

Install via CLI
npx skills add https://github.com/microbus-io/fabric --skill add-task
Repository Details
star Stars 173
call_split Forks 5
navigation Branch main
article Path SKILL.md
More from Creator