---
title: Tasklet API
description: Complete reference for the Tasklet trait and implementations
sidebar:
  order: 4
---

import { Aside, Card, CardGrid, Tabs, TabItem } from '@astrojs/starlight/components';

The `Tasklet` trait defines single-task operations that don't fit the chunk-oriented read-process-write model. Perfect for setup, cleanup, file operations, and maintenance tasks.

## Trait Definition

```rust
pub trait Tasklet {
    /// Executes the tasklet operation
    ///
    /// # Parameters
    /// - `step_execution`: The current step execution context
    ///
    /// # Returns
    /// - `Ok(RepeatStatus)` - Task completed successfully
    /// - `Err(BatchError)` - An error occurred
    fn execute(&self, step_execution: &StepExecution) -> Result<RepeatStatus, BatchError>;
}
```

## RepeatStatus

```rust
pub enum RepeatStatus {
    Finished,    // Task is complete
    Continuable, // Task should be repeated
}
```

<Aside type="tip">
  Return `RepeatStatus::Finished` to complete the step. Return `RepeatStatus::Continuable` to run the tasklet again (useful for polling or incremental work).
</Aside>

---

## Built-in Implementations

Spring Batch RS provides 2 built-in tasklet implementations:

| Tasklet | Feature Flag | Purpose |
|---------|-------------|---------|
| `ZipTasklet` | `zip` | Compress files and directories |
| `FtpTasklet` | `ftp` | Transfer files via FTP/FTPS |

---

## ZIP Tasklet

<Aside type="note">
  **Feature flag:** `zip`
</Aside>

Compresses files and directories into ZIP archives with configurable options.

### Builder Methods

```rust
pub struct ZipTaskletBuilder { /* ... */ }
```

| Method | Type | Default | Description |
|--------|------|---------|-------------|
| `source_path(&Path)` | `&Path` | **required** | File or directory to compress |
| `target_path(&Path)` | `&Path` | **required** | Output ZIP file path |
| `compression_level(u32)` | `u32` | `6` | Compression level (0-9, 0=none, 9=max) |
| `include_pattern(&str)` | `&str` | `"*"` | Glob pattern for files to include |
| `exclude_pattern(&str)` | `&str` | `""` | Glob pattern for files to exclude |
| `preserve_structure(bool)` | `bool` | `true` | Keep directory structure in archive |

### Examples

<Tabs>
  <TabItem label="Basic Compression">
    ```rust
    use spring_batch_rs::tasklet::zip::ZipTaskletBuilder;
    use spring_batch_rs::core::step::StepBuilder;
    use spring_batch_rs::core::job::JobBuilder;

    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let zip_tasklet = ZipTaskletBuilder::new()
            .source_path("/data/exports")
            .target_path("/backups/exports.zip")
            .build()?;

        let step = StepBuilder::new("zip-exports")
            .tasklet(&zip_tasklet)
            .build();

        let job = JobBuilder::new()
            .start(&step)
            .build();

        job.run()?;
        Ok(())
    }
    ```
  </TabItem>
  <TabItem label="With Filtering">
    ```rust
    let zip_tasklet = ZipTaskletBuilder::new()
        .source_path("/logs")
        .target_path("/archives/logs.zip")
        .include_pattern("*.log")         // Only .log files
        .exclude_pattern("*.tmp")         // Exclude .tmp files
        .compression_level(9)             // Maximum compression
        .build()?;
    ```
  </TabItem>
  <TabItem label="Flatten Structure">
    ```rust
    let zip_tasklet = ZipTaskletBuilder::new()
        .source_path("/data")
        .target_path("/flat-archive.zip")
        .preserve_structure(false)        // All files in root of ZIP
        .compression_level(3)             // Fast compression
        .build()?;
    ```
  </TabItem>
  <TabItem label="Single File">
    ```rust
    let zip_tasklet = ZipTaskletBuilder::new()
        .source_path("/data/report.pdf")
        .target_path("/compressed/report.zip")
        .compression_level(9)
        .build()?;
    ```
  </TabItem>
</Tabs>

### Compression Levels

| Level | Speed | Ratio | Use Case |
|-------|-------|-------|----------|
| 0 | Fastest | None | Store only (archiving) |
| 1-3 | Fast | Low | Quick backups, temporary files |
| 4-6 | Balanced | Medium | General purpose (default: 6) |
| 7-9 | Slow | High | Long-term storage, distribution |

### Use Cases

<CardGrid>
  <Card title="Backup Archives" icon="document">
    Compress data directories before backup or transfer
  </Card>
  <Card title="Log Rotation" icon="list">
    Archive old log files to save disk space
  </Card>
  <Card title="Report Distribution" icon="rocket">
    Package reports and attachments for delivery
  </Card>
  <Card title="Cleanup Tasks" icon="setting">
    Compress before deletion as part of data lifecycle
  </Card>
</CardGrid>

---

## FTP Tasklet

<Aside type="note">
  **Feature flag:** `ftp`
</Aside>

Transfers files to/from FTP or FTPS servers with authentication and connection management.

### Builder Methods

```rust
pub struct FtpTaskletBuilder { /* ... */ }
```

| Method | Type | Default | Description |
|--------|------|---------|-------------|
| `host(&str)` | `&str` | **required** | FTP server hostname |
| `port(u16)` | `u16` | `21` | FTP server port |
| `username(&str)` | `&str` | `"anonymous"` | Login username |
| `password(&str)` | `&str` | `""` | Login password |
| `use_tls(bool)` | `bool` | `false` | Enable FTPS (TLS encryption) |
| `local_path(&Path)` | `&Path` | **required** | Local file path |
| `remote_path(&str)` | `&str` | **required** | Remote file path |
| `operation(Operation)` | `Operation` | **required** | Upload or Download |

### Examples

<Tabs>
  <TabItem label="Upload File">
    ```rust
    use spring_batch_rs::tasklet::ftp::{FtpTaskletBuilder, FtpOperation};
    use spring_batch_rs::core::step::StepBuilder;

    fn main() -> Result<(), Box<dyn std::error::Error>> {
        let ftp_tasklet = FtpTaskletBuilder::new()
            .host("ftp.example.com")
            .port(21)
            .username("user")
            .password("password")
            .local_path("/data/export.csv")
            .remote_path("/uploads/export.csv")
            .operation(FtpOperation::Upload)
            .build()?;

        let step = StepBuilder::new("upload-to-ftp")
            .tasklet(&ftp_tasklet)
            .build();

        Ok(())
    }
    ```
  </TabItem>
  <TabItem label="Download File">
    ```rust
    let ftp_tasklet = FtpTaskletBuilder::new()
        .host("ftp.example.com")
        .username("user")
        .password("password")
        .local_path("/downloads/data.csv")
        .remote_path("/exports/data.csv")
        .operation(FtpOperation::Download)
        .build()?;
    ```
  </TabItem>
  <TabItem label="FTPS (Secure)">
    ```rust
    let ftp_tasklet = FtpTaskletBuilder::new()
        .host("ftps.example.com")
        .port(990)                        // FTPS implicit port
        .use_tls(true)                    // Enable TLS encryption
        .username("user")
        .password("password")
        .local_path("/secure/data.xml")
        .remote_path("/secure/data.xml")
        .operation(FtpOperation::Upload)
        .build()?;
    ```
  </TabItem>
</Tabs>

### Operations

```rust
pub enum FtpOperation {
    Upload,    // Transfer from local to remote
    Download,  // Transfer from remote to local
}
```

### Error Handling

The FTP tasklet handles common errors:
- Connection failures
- Authentication errors
- File not found
- Permission denied
- Network timeouts

```rust
// Tasklet will return appropriate BatchError
match step.execute(&mut execution) {
    Ok(_) => println!("Transfer successful"),
    Err(BatchError::Tasklet(msg)) => eprintln!("FTP error: {}", msg),
    Err(e) => eprintln!("Other error: {}", e),
}
```

---

## Custom Tasklet Implementation

You can create custom tasklets for any single-task operation:

### Simple Tasklet

```rust
use spring_batch_rs::core::step::{Tasklet, StepExecution, RepeatStatus};
use spring_batch_rs::error::BatchError;

struct HelloTasklet {
    message: String,
}

impl Tasklet for HelloTasklet {
    fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
        println!("{}", self.message);
        Ok(RepeatStatus::Finished)
    }
}

impl HelloTasklet {
    fn new(message: impl Into<String>) -> Self {
        Self {
            message: message.into(),
        }
    }
}
```

### Database Maintenance Tasklet

```rust
use sqlx::PgPool;

struct DatabaseCleanupTasklet {
    pool: PgPool,
}

impl Tasklet for DatabaseCleanupTasklet {
    fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
        // Execute cleanup query
        let runtime = tokio::runtime::Runtime::new()
            .map_err(|e| BatchError::Tasklet(format!("Runtime error: {}", e)))?;

        runtime.block_on(async {
            sqlx::query("DELETE FROM logs WHERE created_at < NOW() - INTERVAL '30 days'")
                .execute(&self.pool)
                .await
                .map_err(|e| BatchError::Tasklet(format!("Cleanup failed: {}", e)))?;

            Ok(RepeatStatus::Finished)
        })
    }
}
```

### File Cleanup Tasklet

```rust
use std::fs;
use std::path::Path;
use std::time::{SystemTime, Duration};

struct FileCleanupTasklet {
    directory: String,
    age_days: u64,
}

impl Tasklet for FileCleanupTasklet {
    fn execute(&self, step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
        let dir = Path::new(&self.directory);
        let max_age = Duration::from_secs(self.age_days * 24 * 60 * 60);
        let now = SystemTime::now();

        let entries = fs::read_dir(dir)
            .map_err(|e| BatchError::Tasklet(format!("Failed to read directory: {}", e)))?;

        let mut deleted = 0;

        for entry in entries {
            let entry = entry.map_err(|e| BatchError::Tasklet(e.to_string()))?;
            let metadata = entry.metadata()
                .map_err(|e| BatchError::Tasklet(e.to_string()))?;

            if let Ok(modified) = metadata.modified() {
                if let Ok(age) = now.duration_since(modified) {
                    if age > max_age {
                        fs::remove_file(entry.path())
                            .map_err(|e| BatchError::Tasklet(e.to_string()))?;
                        deleted += 1;
                    }
                }
            }
        }

        println!("Deleted {} old files", deleted);
        Ok(RepeatStatus::Finished)
    }
}
```

### HTTP API Call Tasklet

```rust
struct ApiNotificationTasklet {
    webhook_url: String,
    message: String,
}

impl Tasklet for ApiNotificationTasklet {
    fn execute(&self, step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
        let client = reqwest::blocking::Client::new();

        let response = client
            .post(&self.webhook_url)
            .json(&serde_json::json!({
                "message": self.message,
                "step": step_execution.step_name(),
                "status": "completed"
            }))
            .send()
            .map_err(|e| BatchError::Tasklet(format!("HTTP request failed: {}", e)))?;

        if !response.status().is_success() {
            return Err(BatchError::Tasklet(
                format!("API returned error: {}", response.status())
            ));
        }

        Ok(RepeatStatus::Finished)
    }
}
```

---

## Repeatable Tasklets

Use `RepeatStatus::Continuable` for tasks that need multiple executions:

```rust
use std::sync::Mutex;

struct PollingTasklet {
    attempts: Mutex<u32>,
    max_attempts: u32,
}

impl Tasklet for PollingTasklet {
    fn execute(&self, _step_execution: &StepExecution) -> Result<RepeatStatus, BatchError> {
        let mut attempts = self.attempts.lock().unwrap();
        *attempts += 1;

        println!("Poll attempt {}/{}", *attempts, self.max_attempts);

        // Check some condition
        if check_condition() {
            println!("Condition met!");
            return Ok(RepeatStatus::Finished);
        }

        if *attempts >= self.max_attempts {
            return Err(BatchError::Tasklet(
                "Max polling attempts reached".to_string()
            ));
        }

        // Wait before next attempt
        std::thread::sleep(std::time::Duration::from_secs(5));

        Ok(RepeatStatus::Continuable)
    }
}

fn check_condition() -> bool {
    // Check if some external condition is met
    false
}
```

<Aside type="caution">
  Be careful with `Continuable` - always include a maximum attempt counter to avoid infinite loops.
</Aside>

---

## Multi-Step Jobs with Tasklets

Combine tasklets with chunk-oriented steps:

```rust
use spring_batch_rs::core::job::JobBuilder;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Step 1: Download file via FTP
    let download_tasklet = FtpTaskletBuilder::new()
        .host("ftp.example.com")
        .username("user")
        .password("pass")
        .remote_path("/data/input.csv")
        .local_path("/temp/input.csv")
        .operation(FtpOperation::Download)
        .build()?;

    let download_step = StepBuilder::new("download")
        .tasklet(&download_tasklet)
        .build();

    // Step 2: Process the file (chunk-oriented)
    let reader = CsvItemReaderBuilder::new()
        .from_path("/temp/input.csv")?;

    let writer = JsonItemWriterBuilder::<Record>::new()
        .from_path("/temp/output.json")?;

    let process_step = StepBuilder::new("process")
        .chunk(100)
        .reader(&reader)
        .writer(&writer)
        .build();

    // Step 3: Compress output
    let zip_tasklet = ZipTaskletBuilder::new()
        .source_path("/temp/output.json")
        .target_path("/output/data.zip")
        .compression_level(9)
        .build()?;

    let compress_step = StepBuilder::new("compress")
        .tasklet(&zip_tasklet)
        .build();

    // Step 4: Upload result
    let upload_tasklet = FtpTaskletBuilder::new()
        .host("ftp.example.com")
        .username("user")
        .password("pass")
        .local_path("/output/data.zip")
        .remote_path("/results/data.zip")
        .operation(FtpOperation::Upload)
        .build()?;

    let upload_step = StepBuilder::new("upload")
        .tasklet(&upload_tasklet)
        .build();

    // Create job with all steps
    let job = JobBuilder::new()
        .start(&download_step)
        .next(&process_step)
        .next(&compress_step)
        .next(&upload_step)
        .build();

    job.run()?;
    Ok(())
}
```

---

## Best Practices

<CardGrid>
  <Card title="Single Responsibility" icon="star">
    Each tasklet should do one thing well. Create multiple tasklets for complex workflows.
  </Card>
  <Card title="Idempotency" icon="approve-check">
    Design tasklets to be safely re-runnable in case of failures.
  </Card>
  <Card title="Error Reporting" icon="warning">
    Return detailed error messages that help diagnose issues.
  </Card>
  <Card title="Resource Cleanup" icon="setting">
    Always clean up resources even if errors occur (use `Drop` trait if needed).
  </Card>
</CardGrid>

## When to Use Tasklets

Use tasklets for:
- File operations (copy, move, delete, compress)
- FTP/SFTP transfers
- API calls or webhooks
- Database maintenance (vacuum, analyze, cleanup)
- Email notifications
- Directory creation/cleanup
- Pre/post processing tasks

Use chunk-oriented steps for:
- Reading and transforming large datasets
- ETL operations
- Data validation and filtering
- Bulk inserts/updates

## See Also

- [Step Configuration](/spring-batch-rs/processing-models/) - Understanding steps and execution
- [Job Builder](/spring-batch-rs/getting-started/) - Creating multi-step jobs
- [Tasklet Examples](/spring-batch-rs/examples/tasklets/) - Practical tasklet examples
- [Error Handling](/spring-batch-rs/error-handling/) - Handling tasklet failures
