diff --git a/src/core/input.jl b/src/core/input.jl
index 803c9ab8121aaec2abcbd9de2a6c6f0e620c5424..e479b844c5826db0b7c404bf69a4ebfd5f058c68 100644
--- a/src/core/input.jl
+++ b/src/core/input.jl
@@ -41,12 +41,12 @@ macro param(domainparam)
 end
 
 """
-    getsettings(configfile, seed=nothing)
+    getsettings(configfile, userparams=Dict())
 
 Combines all configuration options to produce a single settings dict.
-Precedence: commandline parameters - user config file - default values
+Precedence: function arguments - commandline parameters - user config file - default values
 """
-function getsettings(configfile::String, seed::Union{Int64,Nothing}=nothing)
+function getsettings(configfile::String, userparams::Dict{String,Any}=Dict())
     # read in and merge configurations from the commandline, the default config file
     # and a config file supplied by the user (via the --configfile option)
     defaults::Dict{String, Any} = TOML.parsefile(PARAMFILE) |> flattenTOML
@@ -61,7 +61,9 @@ function getsettings(configfile::String, seed::Union{Int64,Nothing}=nothing)
     end
     settings = deepcopy(defaults)
     for param in keys(defaults)
-        if split(param, ".")[2] in keys(commandline)
+        if param in keys(userparams)
+            settings[param] = userparams[param]
+        elseif split(param, ".")[2] in keys(commandline)
             settings[param] = commandline[split(param, ".")[2]]
         elseif !isnothing(configs) && param in keys(configs)
             settings[param] = configs[param]
@@ -73,7 +75,6 @@ function getsettings(configfile::String, seed::Union{Int64,Nothing}=nothing)
             end
         end
     end
-    !isnothing(seed) && (settings["core.seed"] = seed)
     settings["internal.scanparams"] = scanparams
     preprocessparameters(settings, defaults["core.outdir"])
 end
@@ -85,6 +86,7 @@ Take the raw input parameters and process them where necessary (e.g. convert typ
 perform checks). This is a helper function for [`getsettings`](@ref).
 """
 function preprocessparameters(settings::Dict{String,Any}, defaultoutdir::String)
+    #TODO replace errors with exceptions
     # miscellaneous processing
     (settings["core.seed"] == 0) && (settings["core.seed"] = abs(rand(RandomDevice(), Int32)))
     settings["world.mapresolution"] = settings["world.mapresolution"] * 1m
@@ -107,15 +109,15 @@ function preprocessparameters(settings::Dict{String,Any}, defaultoutdir::String)
             settings["crop.cropdirectory"] = abspath(pkgdir(@__MODULE__), settings["crop.cropdirectory"])
             @debug "Using package directory to load crop data: $(settings["crop.cropdirectory"])".
         else
-            Base.error("Couldn't find map directory $(settings["crop.cropdirectory"]).")
+            Base.error("Couldn't find crop directory $(settings["crop.cropdirectory"]).")
         end
     end
     # sanity checks
     if settings["core.startdate"] > settings["core.enddate"]
-        Base.error("Enddate is earlier than startdate.") #TODO replace with exception
+        Base.error("Enddate is earlier than startdate.")
     end
     if !(settings["crop.cropmodel"] in AVAILABLE_CROPMODELS)
-        error("crop.cropmodel = \"$(settings["crop.cropmodel"])\", but has to be one of: $AVAILABLE_CROPMODELS")
+        Base.error("crop.cropmodel = \"$(settings["crop.cropmodel"])\", but has to be one of: $AVAILABLE_CROPMODELS")
     end
     #FIXME enable parallelisation
     # if !isempty(settings["internal.scanparams"]) && (nprocs() < 2)
diff --git a/src/core/simulation.jl b/src/core/simulation.jl
index 4d227a07d636019a1a3aacfcf75a50d7c2d7ce8a..b19f4024a459ef835feb6e2eed0f636528a1a97e 100644
--- a/src/core/simulation.jl
+++ b/src/core/simulation.jl
@@ -47,15 +47,15 @@ function stepagent!(agent::ModelAgent, model::SimulationModel)
 end
 
 """
-    simulate(config=PARAMFILE, seed=nothing)
+    simulate(configfile=PARAMFILE, params=Dict())
 
 Initialise one or more model objects and carry out a full simulation experiment,
-optionally specifying a configuration file and a seed for the RNG.
+optionally specifying a configuration file and/or specific parameters.
 
 This is the default way to run a Persefone simulation.
 """
-function simulate(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing)
-    models = initialise(config, seed)
+function simulate(;configfile::String=PARAMFILE, params::Dict{String,Any}=Dict{String,Any}())
+    models = initialise(configfile=configfile, params=params)
     isa(models, Vector) ? 
         map(simulate!, models) : #TODO parallelise
         simulate!(models)
@@ -68,25 +68,24 @@ Carry out a complete simulation run using a pre-initialised model object.
 """
 function simulate!(model::SimulationModel)
     @info "Simulation run started at $(Dates.now())."
-    runtime = Dates.value(@param(core.enddate)-@param(core.startdate))+1
-    for d in 1:runtime
+    while model.date <= @param(core.enddate)
         stepsimulation!(model)
     end
     finalise!(model)
 end
 
 """
-    initialise(config=PARAMFILE, seed=nothing)
+    initialise(configfile=PARAMFILE, params=Dict())
 
 Initialise the model: read in parameters, create the output data directory,
 and instantiate the SimulationModel object(s). Optionally allows specifying the
-configuration file and overriding the `seed` parameter. This returns a single
+configuration file and overriding specific parameters. This returns a single
 model object, unless the config file contains multiple values for one or more
 parameters, in which case it creates a full-factorial simulation experiment
 and returns a vector of model objects.
 """
-function initialise(config::String=PARAMFILE, seed::Union{Int64,Nothing}=nothing)
-    settings = getsettings(config, seed)
+function initialise(;configfile::String=PARAMFILE, params::Dict{String,Any}=Dict{String,Any}())
+    settings = getsettings(configfile, params)
     scanparams = settings["internal.scanparams"]
     delete!(settings, "internal.scanparams")
     isempty(scanparams) ?
diff --git a/test/simulation_tests.jl b/test/simulation_tests.jl
index fce2f2b2e05cef391a9295ed9a0db25ce2d523a0..b8bb76ecb48efe6e9b76e645deac852a5142c1c0 100644
--- a/test/simulation_tests.jl
+++ b/test/simulation_tests.jl
@@ -4,8 +4,9 @@
 ###
 
 @testset "Model initialisation" begin
-    model = initialise(TESTPARAMETERS)
+    model = initialise(TESTPARAMETERS, params=Dict("core.visualise"=>false))
     @test typeof(model.settings) == Dict{String, Any}
+    @test @param(core.visualise) == false
     @test model.date == Date(2022,2,1)
     @test typeof(model.landscape) == Matrix{Pixel}
     @test typeof(model.dataoutputs) == Dict{String,DataOutput}