diff --git a/src/crop/almass.jl b/src/crop/almass.jl
index 570305a4e4a3cc9c44724305ca359bd859d39197..6c020cc2eee88f6aee0d96499a74f93323970e7b 100644
--- a/src/crop/almass.jl
+++ b/src/crop/almass.jl
@@ -8,6 +8,7 @@
 module ALMaSS
 
 using Persefone:
+    AnnualDate,
     Management,
     Length,
     cm,
@@ -60,16 +61,17 @@ The type struct for all crops. Currently follows the crop growth model as
 implemented in ALMaSS.
 """
 struct CropType
+    #FIXME this needs thinking about. The sowing and harvest dates belong in the farm model,
+    # not here. Also, we need to harmonise crops across the crop growth models.
     name::String
     group::String
-    minsowdate::Union{Missing,Date}
-    maxsowdate::Union{Missing,Date}
-    minharvestdate::Union{Missing,Date}
-    maxharvestdate::Union{Missing,Date}
+    minsowdate::Union{Missing,AnnualDate}
+    maxsowdate::Union{Missing,AnnualDate}
+    minharvestdate::Union{Missing,AnnualDate}
+    maxharvestdate::Union{Missing,AnnualDate}
     mingrowthtemp::Union{Missing,Float64}
     highnutrientgrowth::Union{Missing,CropCurveParams}
     lownutrientgrowth::Union{Missing,CropCurveParams}
-    #issowable::Union{Function,Bool}
 end
 
 cropname(ct::CropType) = ct.name
@@ -106,7 +108,7 @@ isharvestable(cs::CropState) = cs.mature
 Extend `tryparse` to allow parsing GrowthPhase values.
 (Needed to read in the CSV parameter file.)
 """
-function Base.tryparse(type::Type{GrowthPhase}, str::String)
+function Base.tryparse(::Type{GrowthPhase}, str::String)
     str == "janfirst" ? janfirst :
         str == "sow" ? sow :
         str == "marchfirst" ? marchfirst :
@@ -152,8 +154,8 @@ Parse a CSV file containing the required parameter values for each crop
 """
 function readcropparameters(generalcropfile::String, growthfile::String)
     @debug "Reading crop parameters"
-    cropdata = CSV.File(generalcropfile, missingstring="NA", dateformat="d U",
-                        types=[String,Date,Date,Date,Date,Float64,String])
+    cropdata = CSV.File(generalcropfile, missingstring="NA",
+                        types=[String,AnnualDate,AnnualDate,AnnualDate,AnnualDate,Float64,String])
     growthdata = CSV.File(growthfile, missingstring="NA",
                           types=[Int,String,String,GrowthPhase,String,
                                  Float64,Float64,Float64,Float64])
diff --git a/src/crop/farmplot.jl b/src/crop/farmplot.jl
index d4b5787807b6a2069a9267da040506b3a9a5140f..c10030631ca6323d34e8f4a9130fb39568772a51 100644
--- a/src/crop/farmplot.jl
+++ b/src/crop/farmplot.jl
@@ -37,9 +37,9 @@ end
 Sow the specified crop on the farmplot.
 """
 function sow!(farmplot::FarmPlot, model::SimulationModel, cropname::String)
-    #XXX test if the crop is sowable?
     createevent!(model, farmplot.pixels, sowing)
     sow!(farmplot.cropstate, model, cropname)
+    @debug "Farmer $(farmplot.farmer) sowed $(cropname) on farmplot $(farmplot.id)."
 end
 
 """
@@ -50,6 +50,7 @@ Harvest the crop of this farmplot.
 function harvest!(farmplot::FarmPlot{T}, model::SimulationModel) where T
     createevent!(model, farmplot.pixels, harvesting)
     harvest!(farmplot.cropstate, model)  # TODO: multiply with area to return units of `g`
+    @debug "Farmer $(farmplot.farmer) harvested $(cropname(farmplot)) from farmplot $(farmplot.id)."
 end
 
 """
diff --git a/src/farm/farm.jl b/src/farm/farm.jl
index 32a4d7f68a9894d9023f6d407e97e6b10fd6164c..a3aec898dc10c87b8e071be197ac22a96ed54370 100644
--- a/src/farm/farm.jl
+++ b/src/farm/farm.jl
@@ -4,35 +4,52 @@
 ###    
 
 ##TODO what data do we need to gather from the farm submodel?
+## - area covered by each crop over time
+## - average height of each crop over time
+## - total income per year
+
+#XXX Initially, we're only working with a single simple crop rotation.
+# Later on, we need to figure out how to integrate several.
+const CROPROTATION = ["winter rape", "winter wheat", "maize", "winter barley"]
+
+#FIXME Currently, this is specific to the ALMaSS model. We need to figure out how to generalise it.
 
 """
     Farmer
 
-This is the agent type for the farm ABM. (Not yet implemented.)
+This is the agent type for the farm ABM.
 """
 mutable struct Farmer <: ModelAgent
     #XXX make this into an abstract type and create subtypes for different farm submodels? (#69)
     const id::Int64
-    fields::Vector{Int64} # IDs of the farmplots this farmer owns
-    croprotation::Vector{String} #TODO figure this out
-    totalincome::Float64
+    # farmplots owned by this farmer and their associated crop rotations and next sowing date
+    fields::Vector{Int64}
+    croprotations::Dict{Int64, Vector{String}}
+    sowdates::Dict{Int64, AnnualDate}
+    totalincome::Float64 # accumulated income XXX split up by year?
 end
 
 """
     stepagent!(farmer, model)
 
-Update a farmer by one day.
+Update a farmer by one day. Cycle through all fields and see what management is needed.
 """
 function stepagent!(farmer::Farmer, model::SimulationModel)
     for f in farmer.fields
         field = model.farmplots[f]
         ctype = croptype(field)
         if ctype.group != "semi-natural" && isharvestable(field)
-            harvest!(field, model)
+            @harvest()
             #XXX later: calculate income based on yield and annual price
-            (ctype.group != "grass") && @sow("no growth")
-        elseif cropname(field) == "no growth"
-            #TODO if a field has been harvested, check if the next crop can be sown
+            if ctype.group != "grass"
+                @sow("no growth")
+                cycle!(farmer.croprotations[f]) # advance the crop rotation
+                nextcrop = model.crops[farmer.croprotations[f][1]]
+                farmer.sowdates[f] = @rand(nextcrop.minsowdate:nextcrop.maxsowdate)
+            end
+        elseif cropname(field) == "no growth" && model.date == farmer.sowdates[f]
+            # if a field has been harvested, check if the next crop can be sown
+            @sow(farmer.croprotations[f][1])
         end
     end     
 end
@@ -44,16 +61,22 @@ Initialise the model with a set of farm agents.
 """
 function initfarms!(model::SimulationModel)
     #XXX initially, we only have one farmer controlling all fields in the region
-    farmer = Farmer(1, collect(1:length(model.farmplots)), [], 0)
+    farmer = Farmer(1, collect(1:length(model.farmplots)), Dict(), Dict(), 0)
     model.farmers = [farmer]
     setasides = findsetasides(farmer, model)
     for field in model.farmplots
+        field.farmer = farmer.id
         if isgrassland(field, model)
-            @sow("permanent grassland (seeded)")
+            @sow("permanent grassland (grazed)")
+            farmer.croprotations[field.id] = ["permanent grassland (grazed)"]
         elseif field.id in setasides
             @sow("permanent set-aside")
+            farmer.croprotations[field.id] = ["permanent set-aside"]
         else
-            @sow("no growth")
+            @sow("no growth") # assign each arable field a crop rotation, cycled randomly
+            farmer.croprotations[field.id] = cycle!(deepcopy(CROPROTATION), @rand(0:3))
+            nextcrop = model.crops[farmer.croprotations[field.id][1]]
+            farmer.sowdates[field.id] = @rand(nextcrop.minsowdate:nextcrop.maxsowdate)
         end
     end
 end
@@ -70,7 +93,7 @@ function findsetasides(farmer::Farmer, model::SimulationModel)
                            model.farmplots[farmer.fields]))
     setasidearea = 0m²
     setasides = []
-    for f in farmer.fields #XXX should be sorted smallest-largest for highest efficiency
+    for f in keys(farmer.fields) #XXX should be sorted smallest-largest for highest efficiency
         field = model.farmplots[f]
         isgrassland(field, model) && continue
         push!(setasides, f)