Skip to main content

Implementing Core Plugins

Implementing a core plugin in Flyte allows you to define custom task execution logic that runs directly within the FlytePropeller process. Unlike container-based tasks, core plugins are compiled into the Flyte binary and are ideal for lightweight operations or orchestrating external services.

This guide walks you through building a core plugin by implementing the Plugin interface, using the Sleep plugin as a reference.

Define the Plugin Structure​

First, define a struct that will hold any state or configuration needed by your plugin. For a simple plugin like Sleep, you might only need to track internal state like task start times.

package sleep

import (
"time"
"sync"
"github.com/flyteorg/flyte/v2/flyteplugins/go/tasks/pluginmachinery/core"
)

type Plugin struct {
// Example state: tracking start times for idempotency
taskStartTimes map[string]time.Time
mu sync.Mutex
}

func (p *Plugin) GetID() string {
return "sleep-plugin"
}

func (p *Plugin) GetProperties() core.PluginProperties {
return core.PluginProperties{}
}

Implement the Execution Logic​

The Handle method is the heart of your plugin. It is invoked repeatedly by FlytePropeller until the task reaches a terminal state (Success or Failure).

Critical Requirements:

  1. Non-blocking: Handle must return quickly. Do not perform long-running synchronous work here.
  2. Idempotency: Handle may be called multiple times for the same task execution.

You use the TaskExecutionContext to access task metadata, inputs, and state.

func (p *Plugin) Handle(ctx context.Context, tCtx core.TaskExecutionContext) (core.Transition, error) {
// 1. Read task inputs using the InputReader
inputs, err := tCtx.InputReader().Get(ctx)
if err != nil {
return core.UnknownTransition, err
}

// 2. Determine the current state of the task
// In this example, we check if we've waited long enough
startTime := p.getOrAddTaskStartTime(tCtx)
duration := time.Second * 10 // Simplified logic

if time.Since(startTime) >= duration {
// 3. Transition to Success
return core.DoTransition(core.PhaseInfoSuccess(nil)), nil
}

// 4. Transition to Running to indicate work is still in progress
return core.DoTransition(core.PhaseInfoRunning(core.DefaultPhaseVersion, nil)), nil
}

Manage Task Transitions​

Flyte uses the Transition and PhaseInfo types to manage the task lifecycle. When you return a transition, FlytePropeller updates the task status in the Admin service.

Common transitions include:

  • core.PhaseInfoQueued(version, reason): Task is waiting for resources.
  • core.PhaseInfoRunning(version, info): Task is actively executing.
  • core.PhaseInfoSuccess(info): Task completed successfully.
  • core.PhaseInfoFailure(errorCode, message, info): Task failed with a specific error.

The PhaseInfo struct (defined in flyteplugins/go/tasks/pluginmachinery/core/phase.go) allows you to attach TaskInfo which can include external logs or custom metadata for the Flyte Console.

Handle Abort and Finalize​

You must implement Abort and Finalize to ensure resources are cleaned up.

  • Abort: Called when a user cancels the workflow or a downstream failure occurs.
  • Finalize: Always called after Handle or Abort completes.
func (p *Plugin) Abort(ctx context.Context, tCtx core.TaskExecutionContext) error {
// Clean up external resources if necessary
return nil
}

func (p *Plugin) Finalize(ctx context.Context, tCtx core.TaskExecutionContext) error {
// Final cleanup, e.g., removing internal state
return nil
}

Register the Plugin​

To make Flyte aware of your plugin, register it using the PluginRegistry. This is typically done in an init() function. You provide a LoadPlugin function that receives a SetupContext, which grants access to Kubernetes clients and metrics scopes.

import (
"github.com/flyteorg/flyte/v2/flyteplugins/go/tasks/pluginmachinery"
)

func init() {
pluginmachinery.PluginRegistry().RegisterCorePlugin(
core.PluginEntry{
ID: "sleep",
RegisteredTaskTypes: []core.TaskType{"sleep"},
LoadPlugin: func(ctx context.Context, iCtx core.SetupContext) (core.Plugin, error) {
// Initialize your plugin using SetupContext
return &Plugin{
taskStartTimes: make(map[string]time.Time),
}, nil
},
IsDefault: false,
},
)
}

Complete Example Summary​

By implementing the Plugin interface, you have created a component that:

  1. Registers itself for a specific TaskType via PluginEntry.
  2. Initializes its environment using SetupContext.
  3. Executes non-blocking logic in Handle using TaskExecutionContext.
  4. Communicates state changes back to Flyte using Transition and PhaseInfo.
  5. Ensures reliability through idempotent Abort and Finalize methods.