How to create form letters from spreadsheet data in ConTeXt?

Afaict there is nothing builtin. But as often you can get there by combining some of the existing functionality. All you need is a CSV parser and you can use buffers to do the rest. (I modified the interface a bit so you can simply \insert[Field Name] instead of \insertFieldName.) The usage is as follows:

  1. Define a template. In the revised form, your example code would look like this:

    \startcsvtemplate [tpl]
    Dear \insert[Name],
    
    You owe \insert[Amount]. Please send it before \insert[Date].
    \par
    \stopcsvtemplate
    

    Trailing endlines are stripped, so you will have to request paragraphs explicitly.

  2. Define an input buffer (optional): Input can be read from a file or from a buffer. In the latter case, the buffer needs to be defined, just like any other buffer:

    \startbuffer[csdata]
    Name,Amount,Date
    "Mr. White","\\letterdollar 300","Dec. 2, 1911"
    "Mr. Brown","\\letterdollar 300","Dec. 3, 1911"
    "Ms. Premise","\\letterdollar 42","Dec. 4, 1911"
    "Ms. Conclusion","\\letterdollar 23","Dec. 5, 1911"
    \stopbuffer
    
  3. Request the input to be parsed: Depending on whether you chose to read the data from a buffer or from a file, you will have to process it using the appropriate command:

    \processcsvbuffer[one][csdata]
    \processcsvfile[two][test.csv]
    

    The first argument of either command is the id by which the dataset can be referenced later (similar to \useexternalfigure[a_cow][cow.pdf]).

  4. Now that dataset and template are in place, you can use them together in a job definition:

    \definecsvjob [testing] [
      data=two,
      template=tpl,
    ]
    

    This will generate a macro \testing which you can use in your document to generate the output.

    \starttext \testing \stoptext
    

NB: The answer below can (and probably should, if used frequently) be improved by defining some template language and moving the string processing to Lua entirely. As it is, the performance will be poor due to the repeated calls to Lua from TeX.

Example output.

% macros=mkvi

\unprotect
\startluacode
  local datasets = { }

  local buffersraw   = buffers.raw
  local context      = context
  local ioloaddata   = io.loaddata
  local lpegmatch    = lpeg.match
  local stringformat = string.format
  local stringmatch  = string.match
  local stringsub    = string.sub
  local tableconcat  = table.concat
  local tableswapped = table.swapped

  local die = function (msg) print(msg or "ERROR") os.exit(1) end

  local csv_parser
  do
    --- This is (more than) an RFC 4180 parser.
    --- http://tools.ietf.org/html/rfc4180
    local C, Cg, Cs, Ct, P, S, V
        = lpeg.C, lpeg.Cg, lpeg.Cs, lpeg.Ct, lpeg.P, lpeg.S, lpeg.V

    local backslash = P[[\letterbackslash]]
    local comma     = ","
    local dquote    = P[["]]
    local eol       = S"\n\r"^1
    local noquote   = 1 - dquote
    local unescape  = function (s) return stringsub(s, 2) end
    csv_parser = P{
      "file",
      file    = Ct((V"header" * eol)^-1 * V"records"),
      header  = Cg(Ct(V"name" * (comma * V"name")^0), "header"),
      records = V"record" * (eol * V"record")^0 * eol^0,
      record  = Ct(V"field" * (comma * V"field")^0),
      name    = V"field",
      field   = V"escaped" + V"non_escaped",
      --- Deviate from rfc: the “textdata” terminal was defined only
      --- for 7bit ASCII. Also, any character may occur in a quoted
      --- field as long as it is escaped with a backslash. (\TEX          --- macros start with two backslashes.)
      escaped     = dquote
                  * Cs(((backslash * 1 / unescape) + noquote)^0)
                  * dquote
                  ,
      non_escaped = C((1 - dquote - eol - comma)^0),
    }
  end

  local process = function (id, raw)
    --- buffers may have trailing EOLs
    raw = stringmatch(raw, "^[\n\r]*(.-)[\n\r]*$")
    local data = lpegmatch(csv_parser, raw)
    --- map column name -> column nr
    data.header = tableswapped(data.header)
    datasets[id] = data
  end

  --- escaping hell ahead, please ignore.
  local s_item = [[
  \bgroup
    \string\def\string\insert{\string\getvalue{csv_insert_field}{%s}{%s}}%%
%s%% template
  \egroup
]]

  local typeset = function (id, template)
    local data   = datasets[id] or die("ERROR unknown dataset: " .. id)
    template     = stringmatch(buffersraw(template), "^[\n\r]*(.-)[\n\r]*$")
    local result = { }
    local last = \letterhash data
    for i=1, last do
      result[i] = stringformat(s_item, id, i, template)
    end
    context(tableconcat(result))
  end

  local insert = function (id, n, field)
    local this = datasets[id]
    context(this[n][this.header[field]])
  end

  commands.process_csv      = process
  commands.process_csv_file = function (id, fname)
    process(id, ioloaddata(fname, true))
  end
  commands.typeset_csv_job  = typeset
  commands.insert_csv_field = insert

\stopluacode

\startinterface all
  \setinterfaceconstant{template}{template}
  \setinterfaceconstant    {data}{data}
\stopinterface

\def\processcsvbuffer[#id][#buf]{%
  \ctxcommand{process_csv([[#id]], buffers.raw(\!!bs#buf\!!es))}%
}

\def\processcsvfile[#id][#filename]{%
  \ctxcommand{process_csv_file([[#id]], \!!bs\detokenize{#filename}\!!es)}%
}

%% modeled after \startbuffer
\setuvalue{\e!start csvtemplate}{%
  \begingroup
  \obeylines
  \dosingleempty\csv_template_start%
}

\def\csv_template_start[#id]{%
  \buff_start_indeed{}{#id}{\e!start csvtemplate}{\e!stop csvtemplate}%
}

\installnamespace                  {csvjob}
\installcommandhandler \????csvjob {csvjob} \????csvjob

\appendtoks
  \setuevalue{\currentcsvjob}{\csv_job_direct[\currentcsvjob]}
\to \everydefinecsvjob

\unexpanded\def\csv_job_direct[#id]{%
  \edef\currentcsvjob{#id}%
  \dosingleempty\csv_job_indeed%
}

\def\csv_job_indeed[#setups]{%
  \iffirstargument\setupcurrentcsvjob[#setups]\fi
  \ctxcommand{typeset_csv_job(
                [[\csvjobparameter\c!data]],
                [[\csvjobparameter\c!template]])}%
}

\def\csv_insert_field#id#n[#field]{%
  \ctxcommand{insert_csv_field([[#id]], #n, [[#field]])}%
}

\protect

%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%                               demo
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%

%% Stepwise instructions.
%% step 1: Define template.
\startcsvtemplate [tpl]
Dear \insert[Name],

You owe \insert[Amount]. Please send it before \insert[Date].
\par
\stopcsvtemplate

%% step 2: Define an input (CSV).
\startbuffer[csdata]
Name,Amount,Date
"Mr. White","\\letterdollar 300","Dec. 2, 1911"
"Mr. Brown","\\letterdollar 300","Dec. 3, 1911"
"Ms. Premise","\\letterdollar 42","Dec. 4, 1911"
"Ms. Conclusion","\\letterdollar 23","Dec. 5, 1911"
\stopbuffer

%% step 3: Parse and store the input.
\processcsvbuffer[one][csdata]
%\processcsvfile[two][test.csv]

%% step 4: Declare a job, joining dataset and template.
\definecsvjob [testing] [
  data=two,
  template=tpl,
]

%% step 5: Enjoy!
\starttext 
  \testing
\stoptext

I don't know how to use ConTeXt, but I know that you can use LuaTex in ConTeXt, so I give you a LuaLaTeX example which is easy to port to ConTeXt because the main work is done in Lua. The principle is that you define a template of the letter in the LaTeX/ConTeXt-world including some LaTeX/ConTeXt macros (variables) and call the template repetitively from LuaTeX with each time redefined variables for each entry in the given csv file. Before you can do this you have to read the csv file and store the data in a Lua table.

Maybe it would be a cleaner solution to use arguments (#1,#2,#3,...) in the letter template instead of redefining the variables each time but then the template is not easy to read by the user because he don't know the meaning of the arguments and you are also limited to nine arguments. It's your choice.

\documentclass{article}
\usepackage{filecontents}

\begin{filecontents*}{datafile.csv}
Mr.;Homer;Simpson;Evergreen Terrace, Springfield
Ms.;Marge;Simpson;Evergreen Terrace, Springfield
Mr.;Bart;Simpson;Evergreen Terrace, Springfield
Ms.;Lisa;Simpson;Evergreen Terrace, Springfield
Ms.;Maggie;Simpson;Evergreen Terrace, Springfield
\end{filecontents*}

\begin{filecontents*}{luaFunctions.lua}
function ReadData()

    local input = io.open('datafile.csv', 'r')
    dataTable = {}

    for line in input:lines() do

        -- split the line
        local split = string.explode(line, ";")

        -- store the arguments in variables
        tableItem = {}
        tableItem.Sex = split[1]
        tableItem.FirstName = split[2]
        tableItem.SurName = split[3]
        tableItem.Address = split[4]

        -- insert the arguments of one line in the table
        table.insert(dataTable, tableItem)
    end
end

function Letter(sex, firstName, surName, address)
    -- redefine the latex commands and execute the \letterTemplate macro
    tex.print(string.format("\\def\\sex{%s}",sex))
    tex.print(string.format("\\def\\firstName{%s}",firstName))
    tex.print(string.format("\\def\\surName{%s}",surName))
    tex.print(string.format("\\def\\address{%s}",address))
    tex.print("\\letterTemplate")
end

function PrintLetter()
    -- read the external file and store the data in a table
    ReadData()

    -- loop through the table and print a letter for each table item 
    for i,p in ipairs(dataTable) do
        Letter(p.Sex, p.FirstName, p.SurName, p.Address)

        -- pagebreak
        if i ~= #dataTable then 
            tex.print("\\newpage")
        end       
    end
end
\end{filecontents*}    

\directlua{dofile("luaFunctions.lua")}

\def\letterTemplate{%    
Hallo \sex\ \firstName,\\
your surname is \surName\ and you live at \address.\\

Regards
}

\begin{document}
\directlua{PrintLetter()}
\end{document}