> ## Documentation Index
> Fetch the complete documentation index at: https://docs.raydocs.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Filesystem Deposit Workflows

> Poll S3-compatible, FTP, or SFTP inbox folders, claim files safely, and clean them up inside your workflow.

# Filesystem deposit workflows

Raydocs workflows can now connect to remote filesystems using a single `filesystem` credential type.

Version 1 supports:

* S3-compatible storage
* FTP
* SFTP
* SharePoint document libraries through Microsoft Graph app-only access

This is designed for simple "drop a file in a folder and let the workflow pick it up" integrations.

## The moving parts

There are two workflow building blocks:

* **Filesystem** node: list, read, inspect, move, copy, or delete remote files
* **Filesystem Deposit Trigger**: poll one or more inbox folders and create one workflow run per claimed file

## Supported credentials

The shared `filesystem` credential type supports four drivers:

* **S3-compatible**
* **FTP**
* **SFTP**
* **SharePoint**

The credential form changes depending on the selected driver:

* **S3-compatible** uses fields such as access key, secret, region, bucket, and optional endpoint
* **FTP** uses host, port, username, password, SSL, passive mode, and timeout
* **SFTP** uses host, port, username, password or private key, optional passphrase, and timeout
* **SharePoint** uses tenant ID, drive ID, optional site ID, and optional root path. Microsoft Graph app credentials stay configured on the Raydocs server, not in the customer workflow credential.

When you edit an existing credential:

* Raydocs only pre-fills non-secret fields
* existing secrets are shown as stored, but never returned in clear text
* leaving a secret field empty keeps the current stored value
* clearing a secret is an explicit action in the credential editor

You can reuse the same credential in both:

* the **Filesystem Deposit Trigger**
* the **Filesystem** action node

## How deposit claiming works

The trigger does not use a separate `processing/` folder.

Instead, when a file is picked up, Raydocs renames it in place and appends the workflow run id to the filename.

Example:

* original: `inbox/invoice-123.pdf`
* claimed: `inbox/invoice-123.__raydocs__run_<runId>.pdf`

This gives two important guarantees:

* the original filename no longer appears as a fresh candidate on the next scan
* already-claimed files are easy for the scanner to ignore

## What happens during a scan

Each scan follows the same sequence:

1. Raydocs loads the trigger configuration and acquires a lock for that trigger
2. Raydocs scans the configured inbox folders
3. It filters files using the trigger settings:
   * inbox path list
   * allowed extensions
   * optional path regex
   * recursive or non-recursive scan
   * hidden file filtering
   * max files per scan
4. For each candidate file, Raydocs creates a workflow run
5. Raydocs renames the file in place to claim it
6. Raydocs injects file metadata into the run input
7. Raydocs dispatches the workflow run

The important idea is this:

* one file found
* one workflow run created
* one claimed filename written back to the remote filesystem

## What the workflow receives

When a file is successfully claimed, the workflow run receives structured input.

Example:

```json theme={null}
{
  "deposit_file": {
    "original_path": "inbox/invoice-123.pdf",
    "claimed_path": "inbox/invoice-123.__raydocs__run_123e4567-e89b-12d3-a456-426614174000.pdf",
    "original_filename": "invoice-123.pdf",
    "claimed_filename": "invoice-123.__raydocs__run_123e4567-e89b-12d3-a456-426614174000.pdf",
    "extension": "pdf"
  },
  "deposit_config": {
    "inbox_paths": ["inbox"]
  }
}
```

This means the workflow does **not** need to parse the filename to recover the original path. Both the original and claimed paths are already available in the input.

The trigger output is metadata only. It does **not** download the remote file, store it as a workflow file, or return a `fileRef`.

Use the fields this way:

* `deposit_file.claimed_path`: the remote file path owned by this run; use it for read, move, copy, delete, and cleanup
* `deposit_file.original_path`: the remote path before Raydocs claimed the file; use it for traceability, not for normal cleanup
* `deposit_file.original_filename`: the human-friendly name to use when archiving or moving to an error folder
* `deposit_file.claimed_filename`: the technical claimed name that includes the run id
* `deposit_file.extension`: the normalized file extension, for example `pdf`
* `deposit_config.inbox_paths`: the inbox paths that were configured on the trigger

The most common expression after this trigger is:

```text theme={null}
={{ $input.deposit_file.claimed_path }}
```

Pass that expression to the **Filesystem** node when you want to read or clean up the remote file.

## Reading the claimed file into the workflow

After the trigger claims a file, add a **Filesystem** node with:

* `operation`: `read`
* `source_path`: `={{ $input.deposit_file.claimed_path }}`

The `read` operation copies the remote file content into Raydocs runtime storage and returns a workflow `fileRef`.

Example output:

```json theme={null}
{
  "file": {
    "type": "fileRef",
    "id": "7c80e537-5b0d-4bf2-9db0-7c2a5dd6079a",
    "filename": "invoice.__raydocs__run_123e4567-e89b-12d3-a456-426614174000.pdf",
    "mime": "application/pdf",
    "size": 38142,
    "storageKey": "tmp/workflows/<run-id>/<file-id>_invoice-raydocs-run-123e4567-e89b-12d3-a456-426614174000.pdf",
    "source": {
      "type": "remote_filesystem",
      "path": "inbox/invoice.__raydocs__run_123e4567-e89b-12d3-a456-426614174000.pdf"
    },
    "expiresAt": "2026-04-28T10:30:00.000000Z",
    "label": null,
    "role": null
  },
  "metadata": {
    "size": 38142,
    "last_modified": 1776767400,
    "mime": "application/pdf"
  },
  "source_path": "inbox/invoice.__raydocs__run_123e4567-e89b-12d3-a456-426614174000.pdf"
}
```

Important details:

* `file.type` is always `fileRef`
* `file` is a temporary workflow file handle, not a workspace document
* downstream file-aware nodes should receive the full `file` object, not only `file.id`
* `file.source.path` points back to the claimed remote path
* `expiresAt` is the lifetime of the runtime file reference
* reading the file does **not** remove, rename, archive, or delete the remote file

After `read`, use the returned file with expressions such as:

```text theme={null}
={{ $json.file }}
```

or, if your editor exposes named node outputs:

```text theme={null}
={{ $node["Read claimed file"].file }}
```

## What the trigger does

The trigger is intentionally lightweight. It only handles:

* schedule
* scan
* filter
* claim by rename
* create workflow runs

It does **not** handle:

* archive folders
* error folders
* automatic cleanup after success
* automatic move-to-error behavior

## What happens when a file is found

When the trigger finds a matching file:

* it creates a run
* it claims the file by renaming it
* it starts the workflow with the `deposit_file` payload

At that point, the file is considered owned by that workflow run.

From there, cleanup is the workflow's responsibility.

That is why the trigger stays generic and reusable across different business processes.

## What happens when something goes wrong

There are several different error cases, and they do **not** all behave the same way.

### If the scan cannot access the filesystem

Examples:

* bad credentials
* remote server unavailable
* permission denied on the root path

In this case:

* no files are claimed
* no runs are started
* the trigger records an error and retries on the next scheduled scan

### If a run is created but the claim rename fails

Examples:

* the file was removed by another process
* the remote server rejects the rename
* permissions allow listing but not moving

In this case:

* Raydocs does **not** keep the pending run
* the file stays untouched under its original name
* the next scan can try again naturally

### If the file is claimed but the workflow cannot be started cleanly

Examples:

* run enrichment fails
* dispatch fails after the rename

In this case Raydocs tries to compensate:

* move the file back to its original name
* delete the pending run

If that compensation itself fails, the run is marked as cancelled with error details so the failure is visible instead of being silent.

### If the workflow starts and later fails during processing

This is the most important case to design for.

In this case:

* the file is already claimed
* the trigger will ignore it on future scans
* Raydocs will **not** automatically move it to `error/`

That final cleanup is intentionally left to the workflow.

## Recommended cleanup pattern

Cleanup belongs to the workflow itself.

Recommended approach:

* on the success path, use the **Filesystem** node to delete the claimed file or move it to an archive folder
* on the failure path, use **On Workflow Error** plus the **Filesystem** node to move the claimed file to an `error/` folder

That keeps the trigger generic while still making the deposit pattern safe and reusable.

## Recommended error-handling pattern

For deposit workflows, the safest mental model is:

1. The trigger only finds and claims files
2. The main workflow path handles success cleanup
3. `On Workflow Error` handles failure cleanup

Typical pattern:

* success path:
  * process the claimed file
  * delete it
  * or move it to `archive/`
* error path:
  * move the claimed file to `error/<original_filename>`
  * optionally notify someone

For general workflow-level error routing, see [Workflow Error Handling](/guides/workflow-error-handling).

## Suggested failure strategy

If a workflow fails after the file has been claimed:

* leave the file where it is until your error branch runs
* in `trigger.on_error`, move the claimed file to something like `error/<original_filename>`

This makes it easy for non-technical operators to find and rework failed files the next day.

## Why Raydocs does not move files to `error/` automatically

This is a deliberate design choice.

Different teams want different behavior:

* some want to delete successful files
* some want to archive them
* some want to rename them
* some want to keep the original name in `error/`
* some want to preserve the claimed name for traceability

Because of that, the trigger only handles detection and claiming. The business decision of what to do after processing belongs to the workflow itself.

## Example workflow patterns

### Pattern 1: Simple deposit then delete

* trigger on files in `inbox/`
* read the claimed file with the **Filesystem** node
* process it
* delete `={{ $input.deposit_file.claimed_path }}`

Use this when the remote inbox is only a transport channel and you do not need a remote audit trail after success.

### Pattern 2: Deposit then archive

* trigger on files in `inbox/`
* process the file
* move `={{ $input.deposit_file.claimed_path }}` to `=archive/{{ $input.deposit_file.original_filename }}`

This restores the human-facing filename in the archive folder while still using the claimed path as the technical source.

### Pattern 3: Deposit with explicit error folder

* trigger on files in `inbox/`
* process the file in the main path
* add **On Workflow Error**
* in the error path, move `={{ $input.deposit_file.claimed_path }}` to `=error/{{ $input.deposit_file.original_filename }}`

This is usually the best pattern for human-operated inboxes.

## The Filesystem node

The **Filesystem** node is the action node you use after the trigger.

It supports these operations:

* `list`
* `read`
* `exists`
* `metadata`
* `copy`
* `move`
* `write`
* `delete`

Common uses in deposit workflows:

* read the claimed file into the workflow
* write raw content or one workflow file reference to the remote filesystem
* move failed files into an error folder
* archive successful files
* delete temporary files after success

`move` can also be used as a rename operation.

Operation outputs:

* `list` returns `entries[]`, `count`, and `source_path`
* `read` returns `file` as a `fileRef`, `metadata`, and `source_path`
* `exists` returns `exists` and `source_path`
* `metadata` returns `metadata` and `source_path`
* `write` returns `success`, `destination_path`, `bytes_written`, and `source`
* `copy` returns `success`, `source_path`, and `destination_path`
* `move` returns `success`, `source_path`, and `destination_path`
* `delete` returns `success` and `source_path`

For deposit workflows, the remote cleanup operations should normally use:

* source path: `={{ $input.deposit_file.claimed_path }}`
* archive destination: `=archive/{{ $input.deposit_file.original_filename }}`
* error destination: `=error/{{ $input.deposit_file.original_filename }}`

## Trigger configuration

The **Filesystem Deposit Trigger** supports:

* `scan_interval_minutes`
* `inbox_paths`
* `allowed_extensions`
* `path_regex`
* `recursive`
* `max_files_per_scan`
* `ignore_hidden_files`

New trigger nodes default to a `10` minute scan interval unless you set a different value.

Use this trigger when you want a generic filesystem inbox, not a fully custom polling engine.

## Practical recommendations

* Start with a single inbox folder and a narrow extension filter such as `pdf`
* Add `path_regex` when filename patterns matter, for example `^inbox/client-a/.*\.pdf$`
* Use `max_files_per_scan` to avoid huge bursts on the first import
* Prefer `On Workflow Error` over ad hoc local cleanup on every node
* Keep operator-facing failed files in an explicit `error/` folder
* Use the claimed path for technical cleanup and the original filename for human-facing naming
