Update dynamic column names using keys from jsonb argument
Your case is too dynamic for json_populate_record(): it takes a record type which you don't know (nor have) at the time of calling.

PROCEDURE calling UPDATE for each JSON key

Proof of concept to show what did not work in your attempt.

CREATE PROCEDURE record_event (
   foo_arg int,
   name_arg text,
   data_arg jsonb,
   occurred_at_arg timestamptz
) AS
$proc$
DECLARE
   column_name text;
BEGIN
   INSERT INTO foo_event
        (foo    , name    , data    , occurred_at)
   VALUES(foo_arg, name_arg, data_arg, occurred_at_arg);

   -- works, but inefficiently:
   FOR column_name IN 
      SELECT * FROM jsonb_object_keys(data_arg)
   LOOP
      EXECUTE format('UPDATE foo SET %I = $1 WHERE id = $2', column_name)
      USING data_arg-&gt;&gt;column_name, foo_arg;
   END LOOP;
END
$proc$  LANGUAGE plpgsql;


Call:

CALL record_event(1, 'name_arg', '{"col1":"val1","CoL2":"val2"}', now());


Column names cannot be dynamic, so format the query (with format() for convenience) and use EXECUTE. But values are better provided with the USING clause.

Note the format specifier %I, but the parameters $1 and $2 refer to values provided by the USING clause (not to function parameters!).

But I would not use it. Multiple UPDATE commands are pretty inefficient. Instead use a ...

Function with a single UPDATE

Should be much more efficient.

CREATE OR REPLACE FUNCTION record_event(
   foo_arg int,
   name_arg text,
   data_arg jsonb,
   occurred_at_arg timestamptz
)
  RETURNS void AS
$func$
DECLARE
   _sql text;
BEGIN
   INSERT INTO foo_event
          (foo    , name    , data    , occurred_at)
   VALUES (foo_arg, name_arg, data_arg, occurred_at_arg);

   SELECT INTO _sql
          'UPDATE foo SET '
       || string_agg(format('%I = %L', key, value), ', ')
       || ' WHERE id = ' || foo_arg
   FROM   jsonb_each_text(data_arg);

   IF _sql IS NOT NULL THEN
      -- RAISE NOTICE '%', _sql; -- uncomment instead of EXECUTE to debug
      EXECUTE _sql;
   END IF;
END
$func$  LANGUAGE plpgsql;


Call:

SELECT record_event(1, 'name_arg', '{"col1":"val1","CoL2":"val2"}', now());


We could pass values with another USING clause. But simply concatenate the whole command for convenience.

Using a FUNCTION as there is nothing that would require a PROCEDURE (new since Postgres 11). Would work as procedure too, though.

As always, properly quote identifiers and values to defend against possible SQL injection. I only dare to concatenate foo_arg directly, as an integer value is safe in this regard and works as unquoted numeric literal.

Note that column names are treated as case-sensitive. 


Are PostgreSQL column names case-sensitive?


Related:


SQL injection in Postgres functions vs prepared queries
When to use stored procedure / user-defined function?
How to create a table dynamically based on json data?