diff --git a/db/core/changelog-master.xml b/db/core/changelog-master.xml
index d11dbb0..83712ea 100644
--- a/db/core/changelog-master.xml
+++ b/db/core/changelog-master.xml
@@ -46,5 +46,6 @@
+
diff --git a/db/core/changesets/037-fn-work-program-get-curve-vrow.sql b/db/core/changesets/037-fn-work-program-get-curve-vrow.sql
new file mode 100644
index 0000000..228101a
--- /dev/null
+++ b/db/core/changesets/037-fn-work-program-get-curve-vrow.sql
@@ -0,0 +1,85 @@
+--liquibase formatted sql
+-- PANELS · core · fn_work_program_get: v_row es jsonb, no un record (.value = 42P01)
+
+--changeset panel:core-037-fn-work-program-get-curve-vrow splitStatements:false
+CREATE OR REPLACE FUNCTION core.fn_work_program_get(payload jsonb)
+RETURNS jsonb
+LANGUAGE plpgsql
+SECURITY INVOKER
+SET search_path = core
+AS $$
+DECLARE
+ v_project_id bigint := NULLIF(payload->>'project_id', '')::bigint;
+ v_program jsonb;
+ v_periods jsonb;
+ v_budget_total numeric(14,2);
+ v_curve jsonb;
+BEGIN
+ IF v_project_id IS NULL THEN
+ RETURN core.rpc_err('VALIDATION', 'fn_work_program_get: project_id es obligatorio',
+ jsonb_build_object('fn', 'fn_work_program_get'));
+ END IF;
+
+ SELECT to_jsonb(wp) INTO v_program
+ FROM work_programs wp WHERE wp.project_id = v_project_id;
+
+ IF v_program IS NULL THEN
+ SELECT COALESCE(SUM(amount), 0) INTO v_budget_total FROM budget_items WHERE project_id = v_project_id;
+ RETURN core.rpc_ok(
+ jsonb_build_object(
+ 'program', NULL,
+ 'periods', '[]'::jsonb,
+ 'budget_subtotal', v_budget_total,
+ 'curve', '[]'::jsonb
+ ),
+ format('Proyecto %s sin programa de obra', v_project_id),
+ jsonb_build_object('fn', 'fn_work_program_get', 'project_id', v_project_id)
+ );
+ END IF;
+
+ SELECT COALESCE(jsonb_agg(to_jsonb(p) ORDER BY p.period_index), '[]'::jsonb) INTO v_periods
+ FROM work_program_periods p WHERE p.program_id = (v_program->>'id')::bigint;
+
+ SELECT COALESCE(SUM(amount), 0) INTO v_budget_total FROM budget_items WHERE project_id = v_project_id;
+
+ -- Sin FOR + v_row.value (eso lo toma Postgres como tabla.columna → 42P01).
+ -- SUM de montos en subquery; jsonb_agg y el acumulado van en capas distintas
+ -- para no anidar agregados (42803).
+ SELECT COALESCE(jsonb_agg(to_jsonb(y) ORDER BY y.period_index), '[]'::jsonb)
+ INTO v_curve
+ FROM (
+ SELECT
+ x.period_index,
+ x.label,
+ x.amount,
+ CASE WHEN v_budget_total > 0 THEN round(x.amount / v_budget_total, 6) ELSE 0 END AS pct_period,
+ CASE WHEN v_budget_total > 0
+ THEN round(SUM(x.amount) OVER (ORDER BY x.period_index) / v_budget_total, 6)
+ ELSE 0
+ END AS pct_cumulative
+ FROM (
+ SELECT
+ pr.period_index,
+ pr.label,
+ round(COALESCE(SUM(pa.amount), 0), 2) AS amount
+ FROM work_program_periods pr
+ LEFT JOIN work_program_partida_amounts pa ON pa.period_id = pr.id
+ WHERE pr.program_id = (v_program->>'id')::bigint
+ GROUP BY pr.period_index, pr.label
+ ) x
+ ) y;
+
+ RETURN core.rpc_ok(
+ jsonb_build_object(
+ 'program', v_program,
+ 'periods', v_periods,
+ 'budget_subtotal', v_budget_total,
+ 'curve', v_curve
+ ),
+ format('Programa de obra del proyecto %s', v_project_id),
+ jsonb_build_object('fn', 'fn_work_program_get', 'project_id', v_project_id)
+ );
+EXCEPTION WHEN OTHERS THEN
+ RETURN core.rpc_from_exception('fn_work_program_get', SQLSTATE, SQLERRM);
+END;
+$$;