Project 05 — Organize Files Automatically With Date Stamps
Stop overwriting files silently. Build a pipeline that checks if a file exists before copying, date-stamps the output, cleans the landing zone automatically, and logs what was missing — a complete production file management workflow.
Quick answer
This project adds safety to ingestion: Get Metadata checks whether a file exists before copying, an If Condition branches into a happy path and an error path, and a success arrow ensures a Delete only runs after a Copy succeeds, so files are never lost.
- Get Metadata prevents cryptic crashes on missing files.
- If Condition splits the pipeline into True and False branches.
- Connect Delete to Copy on success, so a failed Copy keeps the source.
Get Metadata (exists?)
└─ If Condition
True → Copy → (on success) Delete source
False → log "file not found" and stopAzure DE — Zero to Advanced
05 of 25
Beginner+
90–120 min
🏢 Real World Problem
FreshCart's data lake is growing. After Projects 01–04, files are landing in ADLS — but new problems are showing up.
Problem 1 — Files get overwritten silently
Every morning, cities.csv is downloaded and saved to raw/external/cities/cities.csv. But what happened to yesterday's file? Overwritten. Gone. No history. Three months later the data team asks what external price data looked like in January — the answer is: we don't know.
Problem 2 — Nobody knows if the source file actually arrived
The pipeline runs at midnight. What if the supplier server was down? ADF will try to copy a file that doesn't exist and fail with a cryptic error. Better behavior: check first, skip gracefully if missing — don't crash the whole pipeline.
Problem 3 — Landing zone is filling up with processed files
Files land in landing/store_sales/ and stay there forever after being copied to raw/. The landing zone is a staging area, not permanent storage. Processed files should be deleted after a successful copy.
The solution — a production-grade file management pattern:
Step 1: Check if file exists in landing zone (Get Metadata + If Condition)
Step 2: If file exists → copy it to ADLS with date stamp in the name
Step 3: After successful copy → delete file from landing zone
Step 4: If file does not exist → log warning, continue gracefully🧠 Concepts You Must Understand First
What is the Get Metadata Activity?
Get Metadata reads information about a file or folder — without reading the file contents. Think of it like checking a package label before opening it.
Get Metadata can return:
exists → true or false
size → file size in bytes
lastModified → when the file was last changed
itemName → the file name
itemType → "File" or "Folder"
childItems → list of files inside a folderWhat is the If Condition Activity?
ADF's decision maker. It evaluates a TRUE/FALSE expression and runs different activities for each outcome.
IF (file exists AND size > 0)
THEN → Copy the file + Delete original
ELSE → Log a warning message, skip this file
In ADF:
If Condition Activity
├── Expression: @and(activity('get_metadata_store_file').output.exists, greater(...size, 0))
├── True branch: Copy Activity → Delete Activity
└── False branch: Set Variable Activity (log "file not found")What is the Delete Activity?
Deletes a file or folder from a storage location. After successfully copying a file from landing → ADLS, we use Delete to remove the original.
String Interpolation for Date-Stamped File Names
Original: store_ST001_sales.csv
Stamped: store_ST001_sales_20240115.csv
Expression:
@{replace(item(), '.csv', '')}_@{formatDateTime(pipeline().parameters.run_date, 'yyyyMMdd')}.csv
Breaking down:
@{replace(item(), '.csv', '')} → removes .csv → "store_ST001_sales"
_ → literal underscore
@{formatDateTime(..., 'yyyyMMdd')} → "20240115"
.csv → adds .csv back
Result: store_ST001_sales_20240115.csvWhat We Are Building — Visualized
PIPELINE: pl_file_management_daily
FOR EACH store file:
│
├─► GET METADATA
│ "Does store_ST001_sales_20240115.csv exist in landing?"
│
├─► IF CONDITION
│ "output.exists = true AND size > 0?"
│
├─► TRUE BRANCH:
│ │
│ ├─► COPY ACTIVITY
│ │ landing/store_sales/date=2024-01-15/store_ST001_sales_20240115.csv
│ │ → raw/sales/date=2024-01-15/store_ST001_sales_20240115.csv
│ │
│ └─► DELETE ACTIVITY
│ Delete: landing/store_sales/date=2024-01-15/store_ST001_sales_20240115.csv
│
└─► FALSE BRANCH:
SET VARIABLE: missing_files += "ST001 missing for 2024-01-15 | "
(pipeline continues — does not crash)📋 Step by Step Overview
PHASE 1 — Prepare (10 min)
Step 1: Upload test files to landing zone
PHASE 2 — Create New Datasets (15 min)
Step 2: Confirm existing source dataset (reuse from Project 03)
Step 3: Create Delete activity dataset
PHASE 3 — Build the Pipeline (60 min)
Step 4: Create pipeline with parameters and variables
Step 5: Add Set Variable — build run_date_folder
Step 6: Add ForEach activity
Step 7: Inside ForEach — add Get Metadata activity
Step 8: Inside ForEach — add If Condition activity
Step 9: Inside True branch — add Copy activity
Step 10: Inside True branch — add Delete activity
Step 11: Inside False branch — add Set Variable (log missing files)
Step 12: Add final summary log activity to main canvas
Step 13: Validate and Debug — file exists scenario
Step 14: Debug — file missing scenario
Step 15: PublishPhase 1 — Prepare
Step 1 — Upload Test Files to Landing Zone
We need files in the landing zone to test both scenarios — exists AND missing.
Azure Portal → Storage → stfreshmartdev → Containers → landing → store_sales/
Click "+ Add Directory"
Directory name: date=2024-01-15Add Directory dialog — date=2024-01-15 entered
Click into date=2024-01-15 → click "Upload"
Upload only 5 of the 10 store files (ST001–ST005). This lets us test the missing scenario for ST006–ST010.
store_ST001_sales_20240115.csv
store_ST002_sales_20240115.csv
store_ST003_sales_20240115.csv
store_ST004_sales_20240115.csv
store_ST005_sales_20240115.csvUpload dialog — 5 files selected, ready to upload
landing/store_sales/date=2024-01-15/ — showing exactly 5 files (ST001 through ST005)
Phase 2 — Create New Datasets
Step 2 — Reuse Existing Source Dataset
The Get Metadata activity will reuse ds_src_blob_dated_store_sales from Project 03 — it already has run_date_folder and file_name parameters. No new source dataset needed.
Author → Datasets — ds_src_blob_dated_store_sales already exists from Project 03
Step 3 — Create Delete Activity Dataset
In ADF Studio → Author → Datasets → "+" → "New dataset"
Search "Azure Blob Storage" → select → "Continue"
Select "DelimitedText" → "Continue"
Name: ds_delete_blob_landing
Linked service: ls_blob_freshmart_landing
File path: (leave all empty)
First row as header: ✅ Yes
Import schema: NoneClick "OK" → "Parameters" tab → add TWO parameters:
Parameter 1:
Name: run_date_folder
Type: String
Parameter 2:
Name: file_name
Type: Stringds_delete_blob_landing Parameters tab — run_date_folder and file_name parameters
Click "Connection" tab:
Container: landing
Directory → "Add dynamic content":
store_sales/@{dataset().run_date_folder}File → "Add dynamic content":
@dataset().file_nameds_delete_blob_landing Connection tab — landing/store_sales/@{dataset().run_date_folder}/@dataset().file_name
Click 💾 Save
Phase 3 — Build the Pipeline
Step 4 — Create Pipeline With Parameters and Variables
In ADF Studio → Author → "+" → "New pipeline"
Name: pl_file_management_daily
Description: Checks file existence, copies with date stamp, deletes from landing zoneClick empty canvas → "Parameters" tab → add TWO parameters:
Parameter 1:
Name: run_date
Type: String
Default: 2024-01-15
Parameter 2:
Name: store_ids
Type: Array
Default: ["ST001","ST002","ST003","ST004","ST005","ST006","ST007","ST008","ST009","ST010"]Pipeline Parameters tab — run_date and store_ids parameters
Click "Variables" tab → add THREE variables:
Variable 1:
Name: run_date_folder
Type: String
Variable 2:
Name: missing_files
Type: String
Variable 3:
Name: final_log
Type: StringADF does not allow a Set Variable activity to read and write the same variable in one step — it calls this a "self-reference" and throws an error.
missing_files is written to inside the ForEach (appending each missing store).final_log is written to after the ForEach (reads missing_files and builds the summary).Two different variables = no self-reference = no error.
Variables tab — three variables: run_date_folder, missing_files, final_log
Step 5 — Add Set Variable: Build run_date_folder
From left panel → "General" → drag "Set variable" onto the canvas
Click it → bottom panel:
General tab:
Name: set_run_date_folder
Description: Formats run_date into Hive partition folder nameClick "Variables" tab:
Name: run_date_folder
Value: (Add dynamic content)date=@{pipeline().parameters.run_date}Set variable activity — name 'set_run_date_folder', value showing date=@{pipeline().parameters.run_date}
Step 6 — Add ForEach Activity
From left panel → "Iteration & conditionals" → drag "ForEach" onto the canvas
Connect: hover over set_run_date_folder → drag green arrow → connect to ForEach
Canvas — set_run_date_folder connected to ForEach with green success arrow
Click the ForEach → bottom panel:
General tab:
Name: ForEach_stores
Description: Loops through each store ID to check and copy files
Settings tab:
Sequential: ☑ Checked ← IMPORTANT
Items: @pipeline().parameters.store_idsForEach Settings tab — Sequential CHECKED, Items showing @pipeline().parameters.store_ids
Variables in ADF are shared across the pipeline. If two iterations run simultaneously and both try to update
missing_files at the same time, one write overwrites the other — entries get lost. This is a race condition. Sequential solves it — each iteration waits its turn before writing.Parallel risk: Iterations 4 and 6 both find missing files simultaneously
Both try to write to missing_files → one write lost ❌
Sequential: Iteration 4 runs → writes → finishes
Iteration 6 runs → writes → finishes
All updates preserved ✅Step 7 — Inside ForEach: Add Get Metadata Activity
Click the "+" button inside the ForEach box to enter the inner canvas
ForEach box — '+' button inside, about to enter inner canvas
From left panel → "General" → drag "Get Metadata" onto the inner canvas
Get Metadata activity placed on ForEach inner canvas
Click Get Metadata → bottom panel:
General tab:
Name: get_metadata_store_file
Description: Checks if today's store file exists in landing zoneClick "Dataset" tab:
Dataset: ds_src_blob_dated_store_salesrun_date_folder → Add dynamic content:
@variables('run_date_folder')file_name → Add dynamic content:
store_@{item()}_sales_@{formatDateTime(pipeline().parameters.run_date,'yyyyMMdd')}.csvDynamic content editor — full dated file name expression with @item() and formatDateTime
Click "Field list" tab → click "+ New" twice:
Field 1: exists
Field 2: sizeField list tab — 'exists' and 'size' added as fields to retrieve
exists tells you if the file is there. size tells you if it is empty (0 bytes). In production you check both — a 0-byte file exists but contains no data.Step 8 — Inside ForEach: Add If Condition Activity
From left panel → "Iteration & conditionals" → drag "If Condition" onto the inner canvas
Connect green arrow from get_metadata_store_file → connect to If Condition
Inner canvas — get_metadata_store_file connected to If Condition with green arrow
Click If Condition → bottom panel:
General tab:
Name: if_file_exists
Description: Checks if file exists AND has content (size > 0)Click "Activities" tab → Expression field → "Add dynamic content":
@and( activity('get_metadata_store_file').output.exists, greater(activity('get_metadata_store_file').output.size, 0) )Dynamic content editor — the full @and() expression with exists and greater() checks
@and( ... , ... )
→ Returns true only if BOTH conditions are true
activity('get_metadata_store_file').output.exists
→ true if file exists, false if not
→ activity('name') reads output of a previous activity by name
greater(activity('get_metadata_store_file').output.size, 0)
→ true if file size is greater than 0 (not empty)
Combined:
→ true if file EXISTS and is NOT EMPTY ✅
→ false if file is missing OR is 0 bytes ❌If Condition Activities tab — expression showing the @and() check with exists and size
Step 9 — Inside True Branch: Add Copy Activity
Click the pencil icon next to "True" to enter the True branch canvas
If Condition Activities tab — True and False sections, pencil icon next to True highlighted
Drag "Copy data" onto the True branch canvas
Copy data activity placed on the True branch canvas
General tab:
Name: copy_to_adls_with_datestamp
Description: Copies store file from landing to ADLS raw/sales/ with date stampSource tab:
Source dataset: ds_src_blob_dated_store_salesrun_date_folder → @variables('run_date_folder')
file_name →
store_@{item()}_sales_@{formatDateTime(pipeline().parameters.run_date,'yyyyMMdd')}.csvSource tab — both dataset properties filled, file_name showing the dated expression
Sink tab:
Sink dataset: ds_sink_adls_dated_salesrun_date_folder → @variables('run_date_folder')
file_name → same dated expression as Source
store_@{item()}_sales_@{formatDateTime(pipeline().parameters.run_date,'yyyyMMdd')}.csvSink tab — both dataset properties filled matching source
Step 10 — Inside True Branch: Add Delete Activity
Still on the True branch canvas → from left panel → "General" → drag "Delete"
Connect green arrow from copy_to_adls_with_datestamp → connect to Delete
True branch canvas — Copy activity connected to Delete activity with green success arrow
General tab:
Name: delete_from_landing
Description: Removes processed file from landing zone after successful copyClick "Dataset" tab:
Dataset: ds_delete_blob_landingrun_date_folder → @variables('run_date_folder')
file_name → dated file name expression
store_@{item()}_sales_@{formatDateTime(pipeline().parameters.run_date,'yyyyMMdd')}.csvDelete activity Dataset tab — ds_delete_blob_landing selected, both properties filled
Click "Logging settings" tab (optional but recommended):
Enable logging: ✅ Yes
Linked service: ls_adls_freshmart
Log folder path: logs/delete_activity/Delete activity Logging tab — enabled, log path set to logs/delete_activity/
Complete True branch canvas — Copy → Delete connected with green arrow
Step 11 — Inside False Branch: Add Set Variable
Click back arrow → return to If Condition Activities tab → click pencil next to "False"
If Condition — False section pencil icon highlighted
Drag "Set variable" onto the False branch canvas
Set variable activity placed on False branch canvas
General tab:
Name: log_missing_file
Description: Appends missing store ID to missing_files variable for monitoringVariables tab:
Name: missing_files
Value: (Add dynamic content)@{variables('missing_files')}@{item()} missing for @{pipeline().parameters.run_date} | → After ST006 and ST007 missing: "ST006 missing for 2024-01-15 | ST007 missing for 2024-01-15 | "
Set variable — Variables tab with the append expression for missing_files
@{variables('missing_files')} → current value (whatever was already logged)
@{item()} → current store ID, e.g. "ST006"
" missing for " → literal text
@{pipeline().parameters.run_date} → "2024-01-15"
" | " → separator between entries
Each iteration APPENDS — previous entries are preserved ✅Complete False branch canvas — log_missing_file Set variable activity alone
Step 12 — Add Final Summary Log to Main Canvas
Click back arrow until you are on the main pipeline canvas.
From left panel → drag one more "Set variable" onto the main canvas (outside ForEach)
Connect green arrow from ForEach_stores → connect to this new Set variable
General tab:
Name: output_missing_files_log
Description: Final log of all missing files for this pipeline runVariables tab:
Name: final_log ← write to final_log, NOT missing_files
Value: Pipeline run complete. Missing files: @{variables('missing_files')}final_log, not missing_files.ADF throws a "self-reference" error if a Set Variable activity reads and writes the same variable.
missing_files is appended to inside the ForEach. Here we read it and write the result tofinal_log — two different variables, no error.output_missing_files_log — Name shows 'final_log', value reads from missing_files
Main canvas — complete pipeline: set_run_date_folder → ForEach_stores → output_missing_files_log
MAIN CANVAS:
[set_run_date_folder] ──► [ForEach_stores] ──► [output_missing_files_log]
INSIDE ForEach_stores:
[get_metadata_store_file] ──► [if_file_exists]
│
├── TRUE: [copy_to_adls_with_datestamp] ──► [delete_from_landing]
│
└── FALSE: [log_missing_file]Step 13 — Validate
Click "Validate" in the top toolbar
Validation successful — no errors found
"Activity not found" → The If Condition expression uses
activity('get_metadata_store_file') — the name in quotes must exactly match the activity's General tab name. Case-sensitive."Variable is read-only inside parallel ForEach" → ForEach Sequential must be CHECKED ON.
"Delete activity dataset not configured" → True branch → Delete → Dataset tab → confirm
ds_delete_blob_landing is selected with both properties filled.Step 14 — Debug: File Exists Scenario
Click "Debug"
run_date: 2024-01-15
store_ids: ["ST001","ST002","ST003","ST004","ST005","ST006","ST007","ST008","ST009","ST010"]Debug parameter dialog — run_date 2024-01-15, full store_ids array
Click "OK" — pipeline runs sequentially through all 10 stores.
ST001–ST005 go through TRUE branch. ST006–ST010 go through FALSE branch.
Pipeline running — set_run_date_folder green, ForEach running with sequential progress
Pipeline completed — all activities green
Click 👓 glasses icon on ForEach in Output tab:
ForEach iteration list — 10 rows, showing each store ID, which branch ran (TRUE/FALSE), and duration
Verify in ADLS:
raw/sales/date=2024-01-15/
├── store_ST001_sales_20240115.csv ✅
├── store_ST002_sales_20240115.csv ✅
├── store_ST003_sales_20240115.csv ✅
├── store_ST004_sales_20240115.csv ✅
└── store_ST005_sales_20240115.csv ✅
Only 5 files — because only 5 existed in landing. Correct behavior.raw/sales/date=2024-01-15/ — exactly 5 files, matching the stores uploaded
Verify landing zone is cleaned:
landing/store_sales/date=2024-01-15/
(empty — all 5 processed files deleted) ✅landing/store_sales/date=2024-01-15/ — empty folder, files deleted after copying
Check the missing files log — ADF Monitor → pipeline run → click output_missing_files_log → Output:
Pipeline run complete. Missing files: ST006 missing for 2024-01-15 | ST007 missing for 2024-01-15 | ST008 missing for 2024-01-15 | ST009 missing for 2024-01-15 | ST010 missing for 2024-01-15 |output_missing_files_log activity output — showing the missing stores listed in final_log value
Step 15 — Debug: All Files Present Scenario
Upload the remaining 5 store files (ST006–ST010) to landing/store_sales/date=2024-01-15/
landing/store_sales/date=2024-01-15/ — all 10 files now uploaded
Run Debug again with the same parameters. This time all 10 go through TRUE branch.
ForEach iteration list — all 10 rows showing TRUE branch ran, all green
raw/sales/date=2024-01-15/ — all 10 files present
landing/store_sales/date=2024-01-15/ — empty, all files deleted
output_missing_files_log output — 'Pipeline run complete. Missing files: ' (empty — none missing)
Step 16 — Publish
Click "Publish all"
Publishing:
pl_file_management_daily (new)
ds_delete_blob_landing (new)Publish panel — listing new pipeline and dataset
Successfully published notification
🎯 What You Built — Summary
BEFORE:
Pipelines copied blindly — crashed if file was missing
Files accumulated in landing zone forever
No way to know which files were missing on any given day
No history — files overwritten daily
AFTER:
Pipeline checks file existence BEFORE attempting to copy
Landing zone cleaned automatically after successful copy
Missing files logged — you know exactly what did not arrive
Files are date-stamped — full history preserved in ADLS
Pipeline never crashes on missing files — handles gracefullyNew ADF Activities Learned
| Activity | Purpose | Key Output |
|---|---|---|
| Get Metadata | Read file/folder properties | .output.exists, .output.size, .output.lastModified |
| If Condition | Branch based on true/false | Runs True OR False activities |
| Delete | Remove a file from storage | File is permanently removed |
| Set Variable (append) | Build a running log | Concatenates text across iterations |
New Expressions Learned
| Expression | What It Does |
|---|---|
| @and(condition1, condition2) | True only when BOTH conditions are true |
| activity('name').output.fieldname | Read the output of a previous activity |
| greater(value, number) | True if value is greater than number |
| @{replace(string, 'find', 'replace')} | Replace text within a string |
| @{variables('name')}existing@{item()}new | Append text to a variable (concatenation) |
🧠 Key Concepts to Remember
| Concept | What It Is | Why It Matters |
|---|---|---|
| Get Metadata | Reads file properties without reading file | Check existence before copying — prevent crashes |
| If Condition | Branches pipeline based on true/false | Handle missing files gracefully |
| Delete Activity | Permanently removes file from storage | Clean landing zone after successful processing |
| True branch | Activities that run when condition is true | The happy path |
| False branch | Activities that run when condition is false | The error handling path |
| Sequential ForEach | One iteration at a time | Required when writing to shared variables |
| Race condition | Two iterations updating same variable at once | Why parallel ForEach breaks variable updates |
| String concatenation | Appending text to a variable each iteration | Build running logs across loop iterations |
| activity('name').output | Read another activity's result | Core pattern for connecting activity results |
| @and() | Both conditions must be true | Safer than just checking exists alone |
⚠️ Common Mistakes in This Project
| Mistake | Fix |
|---|---|
| Activity name in expression does not match actual name | Expression uses activity('get_metadata_store_file') — if the activity is named GetMetadata1 the expression fails. Names are case-sensitive. |
| ForEach set to Parallel when writing to a variable | Set ForEach Sequential = ON whenever activities inside write to pipeline variables. Parallel causes race conditions. |
| Delete activity runs even when Copy failed | Make sure Delete is connected to Copy with a success arrow (green), not always arrow (grey). Click the arrow to verify it shows "On success". |
| Get Metadata field list not configured | Get Metadata → Field list tab → must explicitly add "exists" as a field. Without this, output.exists returns null. |
| Activities placed in wrong branch | Click Activities tab on If Condition → verify Copy is in True branch and log_missing_file is in False branch. |
| Self-reference error on missing_files variable | Use a second variable (final_log) for the summary. missing_files is written inside ForEach, final_log reads it after. Never read and write the same variable in one Set Variable activity. |
🏆 Tier 1 Complete — What You Have Built So Far
PROJECT 01: Copy a single file → ADF basics, linked services, datasets
PROJECT 02: Copy multiple files with ForEach → Loops, arrays, parallel execution
PROJECT 03: Date-parameterized pipeline + trigger → Parameters, dynamic expressions, scheduling
PROJECT 04: Download from public HTTPS URL → HTTP linked service, internet data sources
PROJECT 05: File management with validation → Get Metadata, If Condition, Delete, error handling🎯 Key Takeaways
- ✓Always use Get Metadata to check file existence before copying — prevents cryptic pipeline crashes
- ✓If Condition branches your pipeline into happy path (True) and error path (False)
- ✓Connect Delete to Copy with a success arrow — if Copy fails, Delete must NOT run
- ✓Sequential ForEach is required when activities inside write to shared pipeline variables
- ✓Use a separate variable for summaries — ADF blocks self-reference (read + write same variable in one step)
- ✓Date-stamping files in ADLS preserves history — overwriting destroys it
- ✓The landing zone is temporary — clean it after processing so it stays lean
🚀 What's Coming in Project 06 — Tier 2 Begins
So far, all data sources have been files — CSVs sitting somewhere waiting to be copied. In the real world, a huge portion of data comes from REST APIs — services you query with an HTTP request and get back structured JSON.
In Project 06, FreshCart integrates with a live public REST API:
- Call a real API endpoint and receive a JSON response
- Extract data and land it in ADLS as a clean CSV
- Handle REST API pagination — when results come in pages
- API key authentication
- Parse nested JSON structures
Quick check
Why connect Delete to Copy with a success arrow, not just place it after?
HELP US IMPROVE
How was this lesson?
Your note is shared with the Chaduvuko team for review. Please don’t include personal or sensitive information.
Spotted a mistake or something unclear? Suggest a fix on GitHub or email us. Select the text first and it is quoted for you.
Discussion
0Have a better approach? Found something outdated? Share it — your knowledge helps everyone learning here.
