diff --git a/lib/pipeline/rte_swx_ctl.c b/lib/pipeline/rte_swx_ctl.c
index dc093860de..86b58e21dc 100644
--- a/lib/pipeline/rte_swx_ctl.c
+++ b/lib/pipeline/rte_swx_ctl.c
@@ -123,12 +123,26 @@ struct selector {
 	struct rte_swx_table_selector_params params;
 };
 
+struct learner {
+	struct rte_swx_ctl_learner_info info;
+	struct rte_swx_ctl_table_match_field_info *mf;
+	struct rte_swx_ctl_table_action_info *actions;
+	uint32_t action_data_size;
+
+	/* The pending default action: this is NOT the current default action;
+	 * this will be the new default action after the next commit, if the
+	 * next commit operation is successful.
+	 */
+	struct rte_swx_table_entry *pending_default;
+};
+
 struct rte_swx_ctl_pipeline {
 	struct rte_swx_ctl_pipeline_info info;
 	struct rte_swx_pipeline *p;
 	struct action *actions;
 	struct table *tables;
 	struct selector *selectors;
+	struct learner *learners;
 	struct rte_swx_table_state *ts;
 	struct rte_swx_table_state *ts_next;
 	int numa_node;
@@ -924,6 +938,70 @@ selector_params_get(struct rte_swx_ctl_pipeline *ctl, uint32_t selector_id)
 	return 0;
 }
 
+static void
+learner_pending_default_free(struct learner *l)
+{
+	if (!l->pending_default)
+		return;
+
+	free(l->pending_default->action_data);
+	free(l->pending_default);
+	l->pending_default = NULL;
+}
+
+
+static void
+learner_free(struct rte_swx_ctl_pipeline *ctl)
+{
+	uint32_t i;
+
+	if (!ctl->learners)
+		return;
+
+	for (i = 0; i < ctl->info.n_learners; i++) {
+		struct learner *l = &ctl->learners[i];
+
+		free(l->mf);
+		free(l->actions);
+
+		learner_pending_default_free(l);
+	}
+
+	free(ctl->learners);
+	ctl->learners = NULL;
+}
+
+static struct learner *
+learner_find(struct rte_swx_ctl_pipeline *ctl, const char *learner_name)
+{
+	uint32_t i;
+
+	for (i = 0; i < ctl->info.n_learners; i++) {
+		struct learner *l = &ctl->learners[i];
+
+		if (!strcmp(learner_name, l->info.name))
+			return l;
+	}
+
+	return NULL;
+}
+
+static uint32_t
+learner_action_data_size_get(struct rte_swx_ctl_pipeline *ctl, struct learner *l)
+{
+	uint32_t action_data_size = 0, i;
+
+	for (i = 0; i < l->info.n_actions; i++) {
+		uint32_t action_id = l->actions[i].action_id;
+		struct action *a = &ctl->actions[action_id];
+
+		if (a->data_size > action_data_size)
+			action_data_size = a->data_size;
+	}
+
+	return action_data_size;
+}
+
 static void
 table_state_free(struct rte_swx_ctl_pipeline *ctl)
 {
@@ -954,6 +1032,14 @@ table_state_free(struct rte_swx_ctl_pipeline *ctl)
 			rte_swx_table_selector_free(ts->obj);
 	}
 
+	/* For each learner table, free its table state. */
+	for (i = 0; i < ctl->info.n_learners; i++) {
+		struct rte_swx_table_state *ts = &ctl->ts_next[i];
+
+		/* Default action data. */
+		free(ts->default_action_data);
+	}
+
 	free(ctl->ts_next);
 	ctl->ts_next = NULL;
 }
@@ -1020,6 +1106,29 @@ table_state_create(struct rte_swx_ctl_pipeline *ctl)
 		}
 	}
 
+	/* Learner tables. */
+	for (i = 0; i < ctl->info.n_learners; i++) {
+		struct learner *l = &ctl->learners[i];
+		struct rte_swx_table_state *ts = &ctl->ts[i];
+		struct rte_swx_table_state *ts_next = &ctl->ts_next[i];
+
+		/* Table object: duplicate from the current table state. */
+		ts_next->obj = ts->obj;
+
+		/* Default action data: duplicate from the current table state. */
+		ts_next->default_action_data = malloc(l->action_data_size);
+		if (!ts_next->default_action_data) {
+			status = -ENOMEM;
+			goto error;
+		}
+
+		memcpy(ts_next->default_action_data,
+		       ts->default_action_data,
+		       l->action_data_size);
+
+		ts_next->default_action_id = ts->default_action_id;
+	}
+
 	return 0;
 
 error:
@@ -1037,6 +1146,8 @@ rte_swx_ctl_pipeline_free(struct rte_swx_ctl_pipeline *ctl)
 
 	table_state_free(ctl);
 
+	learner_free(ctl);
+
 	selector_free(ctl);
 
 	table_free(ctl);
@@ -1251,6 +1362,54 @@ rte_swx_ctl_pipeline_create(struct rte_swx_pipeline *p)
 			goto error;
 	}
 
+	/* learner tables. */
+	ctl->learners = calloc(ctl->info.n_learners, sizeof(struct learner));
+	if (!ctl->learners)
+		goto error;
+
+	for (i = 0; i < ctl->info.n_learners; i++) {
+		struct learner *l = &ctl->learners[i];
+		uint32_t j;
+
+		/* info. */
+		status = rte_swx_ctl_learner_info_get(p, i, &l->info);
+		if (status)
+			goto error;
+
+		/* mf. */
+		l->mf = calloc(l->info.n_match_fields,
+			       sizeof(struct rte_swx_ctl_table_match_field_info));
+		if (!l->mf)
+			goto error;
+
+		for (j = 0; j < l->info.n_match_fields; j++) {
+			status = rte_swx_ctl_learner_match_field_info_get(p,
+				i,
+				j,
+				&l->mf[j]);
+			if (status)
+				goto error;
+		}
+
+		/* actions. */
+		l->actions = calloc(l->info.n_actions,
+			sizeof(struct rte_swx_ctl_table_action_info));
+		if (!l->actions)
+			goto error;
+
+		for (j = 0; j < l->info.n_actions; j++) {
+			status = rte_swx_ctl_learner_action_info_get(p,
+				i,
+				j,
+				&l->actions[j]);
+			if (status || l->actions[j].action_id >= ctl->info.n_actions)
+				goto error;
+		}
+
+		/* action_data_size. */
+		l->action_data_size = learner_action_data_size_get(ctl, l);
+	}
+
 	/* ts. */
 	status = rte_swx_pipeline_table_state_get(p, &ctl->ts);
 	if (status)
@@ -1685,9 +1844,8 @@ table_rollfwd1(struct rte_swx_ctl_pipeline *ctl, uint32_t table_id)
 	action_data = table->pending_default->action_data;
 	a = &ctl->actions[action_id];
 
-	memcpy(ts_next->default_action_data,
-	       action_data,
-	       a->data_size);
+	if (a->data_size)
+		memcpy(ts_next->default_action_data, action_data, a->data_size);
 
 	ts_next->default_action_id = action_id;
 }
@@ -2099,6 +2257,178 @@ selector_abort(struct rte_swx_ctl_pipeline *ctl, uint32_t selector_id)
 	memset(s->groups_pending_delete, 0, s->info.n_groups_max * sizeof(int));
 }
 
+static struct rte_swx_table_entry *
+learner_default_entry_alloc(struct learner *l)
+{
+	struct rte_swx_table_entry *entry;
+
+	entry = calloc(1, sizeof(struct rte_swx_table_entry));
+	if (!entry)
+		goto error;
+
+	/* action_data. */
+	if (l->action_data_size) {
+		entry->action_data = calloc(1, l->action_data_size);
+		if (!entry->action_data)
+			goto error;
+	}
+
+	return entry;
+
+error:
+	table_entry_free(entry);
+	return NULL;
+}
+
+static int
+learner_default_entry_check(struct rte_swx_ctl_pipeline *ctl,
+			    uint32_t learner_id,
+			    struct rte_swx_table_entry *entry)
+{
+	struct learner *l = &ctl->learners[learner_id];
+	struct action *a;
+	uint32_t i;
+
+	CHECK(entry, EINVAL);
+
+	/* action_id. */
+	for (i = 0; i < l->info.n_actions; i++)
+		if (entry->action_id == l->actions[i].action_id)
+			break;
+
+	CHECK(i < l->info.n_actions, EINVAL);
+
+	/* action_data. */
+	a = &ctl->actions[entry->action_id];
+	CHECK(!(a->data_size && !entry->action_data), EINVAL);
+
+	return 0;
+}
+
+static struct rte_swx_table_entry *
+learner_default_entry_duplicate(struct rte_swx_ctl_pipeline *ctl,
+				uint32_t learner_id,
+				struct rte_swx_table_entry *entry)
+{
+	struct learner *l = &ctl->learners[learner_id];
+	struct rte_swx_table_entry *new_entry = NULL;
+	struct action *a;
+	uint32_t i;
+
+	if (!entry)
+		goto error;
+
+	new_entry = calloc(1, sizeof(struct rte_swx_table_entry));
+	if (!new_entry)
+		goto error;
+
+	/* action_id. */
+	for (i = 0; i < l->info.n_actions; i++)
+		if (entry->action_id == l->actions[i].action_id)
+			break;
+
+	if (i >= l->info.n_actions)
+		goto error;
+
+	new_entry->action_id = entry->action_id;
+
+	/* action_data. */
+	a = &ctl->actions[entry->action_id];
+	if (a->data_size && !entry->action_data)
+		goto error;
+
+	/* The table layer provisions a constant action data size per
+	 * entry, which should be the largest data size for all the
+	 * actions enabled for the current table, and attempts to copy
+	 * this many bytes each time a table entry is added, even if the
+	 * specific action requires less data or even no data at all,
+	 * hence we always have to allocate the max.
+	 */
+	new_entry->action_data = calloc(1, l->action_data_size);
+	if (!new_entry->action_data)
+		goto error;
+
+	if (a->data_size)
+		memcpy(new_entry->action_data, entry->action_data, a->data_size);
+
+	return new_entry;
+
+error:
+	table_entry_free(new_entry);
+	return NULL;
+}
+
+int
+rte_swx_ctl_pipeline_learner_default_entry_add(struct rte_swx_ctl_pipeline *ctl,
+					       const char *learner_name,
+					       struct rte_swx_table_entry *entry)
+{
+	struct learner *l;
+	struct rte_swx_table_entry *new_entry;
+	uint32_t learner_id;
+
+	CHECK(ctl, EINVAL);
+
+	CHECK(learner_name && learner_name[0], EINVAL);
+	l = learner_find(ctl, learner_name);
+	CHECK(l, EINVAL);
+	learner_id = l - ctl->learners;
+	CHECK(!l->info.default_action_is_const, EINVAL);
+
+	CHECK(entry, EINVAL);
+	CHECK(!learner_default_entry_check(ctl, learner_id, entry), EINVAL);
+
+	new_entry = learner_default_entry_duplicate(ctl, learner_id, entry);
+	CHECK(new_entry, ENOMEM);
+
+	learner_pending_default_free(l);
+
+	l->pending_default = new_entry;
+	return 0;
+}
+
+static void
+learner_rollfwd(struct rte_swx_ctl_pipeline *ctl, uint32_t learner_id)
+{
+	struct learner *l = &ctl->learners[learner_id];
+	struct rte_swx_table_state *ts_next = &ctl->ts_next[ctl->info.n_tables +
+		ctl->info.n_selectors + learner_id];
+	struct action *a;
+	uint8_t *action_data;
+	uint64_t action_id;
+
+	/* Copy the pending default entry. */
+	if (!l->pending_default)
+		return;
+
+	action_id = l->pending_default->action_id;
+	action_data = l->pending_default->action_data;
+	a = &ctl->actions[action_id];
+
+	if (a->data_size)
+		memcpy(ts_next->default_action_data, action_data, a->data_size);
+
+	ts_next->default_action_id = action_id;
+}
+
+static void
+learner_rollfwd_finalize(struct rte_swx_ctl_pipeline *ctl, uint32_t learner_id)
+{
+	struct learner *l = &ctl->learners[learner_id];
+
+	/* Free up the pending default entry, as it is now part of the table. */
+	learner_pending_default_free(l);
+}
+
+static void
+learner_abort(struct rte_swx_ctl_pipeline *ctl, uint32_t learner_id)
+{
+	struct learner *l = &ctl->learners[learner_id];
+
+	/* Free up the pending default entry, as it is no longer going to be added to the table. */
+	learner_pending_default_free(l);
+}
+
 int
 rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
 {
@@ -2110,6 +2440,7 @@ rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
 
 	/* Operate the changes on the current ts_next before it becomes the new ts. First, operate
 	 * all the changes that can fail; if no failure, then operate the changes that cannot fail.
+	 * We must be able to fully revert all the changes that can fail as if they never happened.
 	 */
 	for (i = 0; i < ctl->info.n_tables; i++) {
 		status = table_rollfwd0(ctl, i, 0);
@@ -2123,9 +2454,15 @@ rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
 			goto rollback;
 	}
 
+	/* Second, operate all the changes that cannot fail. Since nothing can fail from this point
+	 * onwards, the transaction is guaranteed to be successful.
+	 */
 	for (i = 0; i < ctl->info.n_tables; i++)
 		table_rollfwd1(ctl, i);
 
+	for (i = 0; i < ctl->info.n_learners; i++)
+		learner_rollfwd(ctl, i);
+
 	/* Swap the table state for the data plane. The current ts and ts_next
 	 * become the new ts_next and ts, respectively.
 	 */
@@ -2151,6 +2488,11 @@ rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
 		selector_rollfwd_finalize(ctl, i);
 	}
 
+	for (i = 0; i < ctl->info.n_learners; i++) {
+		learner_rollfwd(ctl, i);
+		learner_rollfwd_finalize(ctl, i);
+	}
+
 	return 0;
 
 rollback:
@@ -2166,6 +2508,10 @@ rte_swx_ctl_pipeline_commit(struct rte_swx_ctl_pipeline *ctl, int abort_on_fail)
 			selector_abort(ctl, i);
 	}
 
+	if (abort_on_fail)
+		for (i = 0; i < ctl->info.n_learners; i++)
+			learner_abort(ctl, i);
+
 	return status;
 }
 
@@ -2182,6 +2528,9 @@ rte_swx_ctl_pipeline_abort(struct rte_swx_ctl_pipeline *ctl)
 
 	for (i = 0; i < ctl->info.n_selectors; i++)
 		selector_abort(ctl, i);
+
+	for (i = 0; i < ctl->info.n_learners; i++)
+		learner_abort(ctl, i);
 }
 
 static int
@@ -2460,6 +2809,130 @@ rte_swx_ctl_pipeline_table_entry_read(struct rte_swx_ctl_pipeline *ctl,
 	return NULL;
 }
 
+struct rte_swx_table_entry *
+rte_swx_ctl_pipeline_learner_default_entry_read(struct rte_swx_ctl_pipeline *ctl,
+						const char *learner_name,
+						const char *string,
+						int *is_blank_or_comment)
+{
+	char *token_array[RTE_SWX_CTL_ENTRY_TOKENS_MAX], **tokens;
+	struct learner *l;
+	struct action *action;
+	struct rte_swx_table_entry *entry = NULL;
+	char *s0 = NULL, *s;
+	uint32_t n_tokens = 0, arg_offset = 0, i;
+	int blank_or_comment = 0;
+
+	/* Check input arguments. */
+	if (!ctl)
+		goto error;
+
+	if (!learner_name || !learner_name[0])
+		goto error;
+
+	l = learner_find(ctl, learner_name);
+	if (!l)
+		goto error;
+
+	if (!string || !string[0])
+		goto error;
+
+	/* Memory allocation. */
+	s0 = strdup(string);
+	if (!s0)
+		goto error;
+
+	entry = learner_default_entry_alloc(l);
+	if (!entry)
+		goto error;
+
+	/* Parse the string into tokens. */
+	for (s = s0; ; ) {
+		char *token;
+
+		token = strtok_r(s, " \f\n\r\t\v", &s);
+		if (!token || token_is_comment(token))
+			break;
+
+		if (n_tokens >= RTE_SWX_CTL_ENTRY_TOKENS_MAX)
+			goto error;
+
+		token_array[n_tokens] = token;
+		n_tokens++;
+	}
+
+	if (!n_tokens) {
+		blank_or_comment = 1;
+		goto error;
+	}
+
+	tokens = token_array;
+
+	/*
+	 * Action.
+	 */
+	if (!(n_tokens && !strcmp(tokens[0], "action")))
+		goto other;
+
+	if (n_tokens < 2)
+		goto error;
+
+	action = action_find(ctl, tokens[1]);
+	if (!action)
+		goto error;
+
+	if (n_tokens < 2 + action->info.n_args * 2)
+		goto error;
+
+	/* action_id. */
+	entry->action_id = action - ctl->actions;
+
+	/* action_data. */
+	for (i = 0; i < action->info.n_args; i++) {
+		struct rte_swx_ctl_action_arg_info *arg = &action->args[i];
+		char *arg_name, *arg_val;
+		uint64_t val;
+
+		arg_name = tokens[2 + i * 2];
+		arg_val = tokens[2 + i * 2 + 1];
+
+		if (strcmp(arg_name, arg->name))
+			goto error;
+
+		val = strtoull(arg_val, &arg_val, 0);
+		if (arg_val[0])
+			goto error;
+
+		/* Endianness conversion. */
+		if (arg->is_network_byte_order)
+			val = field_hton(val, arg->n_bits);
+
+		/* Copy to entry. */
+		memcpy(&entry->action_data[arg_offset],
+		       (uint8_t *)&val,
+		       arg->n_bits / 8);
+
+		arg_offset += arg->n_bits / 8;
+	}
+
+	tokens += 2 + action->info.n_args * 2;
+	n_tokens -= 2 + action->info.n_args * 2;
+
+other:
+	if (n_tokens)
+		goto error;
+
+	free(s0);
+	return entry;
+
+error:
+	table_entry_free(entry);
+	free(s0);
+	if (is_blank_or_comment)
+		*is_blank_or_comment = blank_or_comment;
+	return NULL;
+}
+
 static void
 table_entry_printf(FILE *f,
 		   struct rte_swx_ctl_pipeline *ctl,
diff --git a/lib/pipeline/rte_swx_ctl.h b/lib/pipeline/rte_swx_ctl.h
index f37301cf95..2a7d1d57ce 100644
--- a/lib/pipeline/rte_swx_ctl.h
+++ b/lib/pipeline/rte_swx_ctl.h
@@ -52,6 +52,9 @@ struct rte_swx_ctl_pipeline_info {
 	/** Number of selector tables. */
 	uint32_t n_selectors;
 
+	/** Number of learner tables. */
+	uint32_t n_learners;
+
 	/** Number of register arrays. */
 	uint32_t n_regarrays;
 
@@ -512,6 +515,142 @@ rte_swx_ctl_pipeline_selector_stats_read(struct rte_swx_pipeline *p,
 					 const char *selector_name,
 					 struct rte_swx_pipeline_selector_stats *stats);
 
+/*
+ * Learner Table Query API.
+ */
+
+/** Learner table info. */
+struct rte_swx_ctl_learner_info {
+	/** Learner table name. */
+	char name[RTE_SWX_CTL_NAME_SIZE];
+
+	/** Number of match fields. */
+	uint32_t n_match_fields;
+
+	/** Number of actions. */
+	uint32_t n_actions;
+
+	/** Non-zero (true) when the default action is constant, therefore it
+	 * cannot be changed; zero (false) when the default action not constant,
+	 * therefore it can be changed.
+	 */
+	int default_action_is_const;
+
+	/** Learner table size parameter. */
+	uint32_t size;
+};
+
+/**
+ * Learner table info get
+ *
+ * @param[in] p
+ *   Pipeline handle.
+ * @param[in] learner_id
+ *   Learner table ID (0 .. *n_learners* - 1).
+ * @param[out] learner
+ *   Learner table info.
+ * @return
+ *   0 on success or the following error codes otherwise:
+ *   -EINVAL: Invalid argument.
+ */
+__rte_experimental
+int
+rte_swx_ctl_learner_info_get(struct rte_swx_pipeline *p,
+			     uint32_t learner_id,
+			     struct rte_swx_ctl_learner_info *learner);
+
+/**
+ * Learner table match field info get
+ *
+ * @param[in] p
+ *   Pipeline handle.
+ * @param[in] learner_id
+ *   Learner table ID (0 .. *n_learners* - 1).
+ * @param[in] match_field_id
+ *   Match field ID (0 .. *n_match_fields* - 1).
+ * @param[out] match_field
+ *   Learner table match field info.
+ * @return
+ *   0 on success or the following error codes otherwise:
+ *   -EINVAL: Invalid argument.
+ */
+__rte_experimental
+int
+rte_swx_ctl_learner_match_field_info_get(struct rte_swx_pipeline *p,
+					 uint32_t learner_id,
+					 uint32_t match_field_id,
+					 struct rte_swx_ctl_table_match_field_info *match_field);
+
+/**
+ * Learner table action info get
+ *
+ * @param[in] p
+ *   Pipeline handle.
+ * @param[in] learner_id
+ *   Learner table ID (0 .. *n_learners* - 1).
+ * @param[in] learner_action_id
+ *   Action index within the set of learner table actions (0 .. learner table n_actions - 1). Not
+ *   to be confused with the pipeline-leve action ID (0 .. pipeline n_actions - 1), which is
+ *   precisely what this function returns as part of the *learner_action*.
+ * @param[out] learner_action
+ *   Learner action info.
+ * @return
+ *   0 on success or the following error codes otherwise:
+ *   -EINVAL: Invalid argument.
+ */
+__rte_experimental
+int
+rte_swx_ctl_learner_action_info_get(struct rte_swx_pipeline *p,
+				    uint32_t learner_id,
+				    uint32_t learner_action_id,
+				    struct rte_swx_ctl_table_action_info *learner_action);
+
+/** Learner table statistics. */
+struct rte_swx_learner_stats {
+	/** Number of packets with lookup hit. */
+	uint64_t n_pkts_hit;
+
+	/** Number of packets with lookup miss. */
+	uint64_t n_pkts_miss;
+
+	/** Number of packets with successful learning. */
+	uint64_t n_pkts_learn_ok;
+
+	/** Number of packets with learning error. */
+	uint64_t n_pkts_learn_err;
+
+	/** Number of packets with forget event. */
+	uint64_t n_pkts_forget;
+
+	/** Number of packets (with either lookup hit or miss) per pipeline action. Array of
+	 * pipeline *n_actions* elements indedex by the pipeline-level *action_id*, therefore this
+	 * array has the same size for all the tables within the same pipeline.
+	 */
+	uint64_t *n_pkts_action;
+};
+
+/**
+ * Learner table statistics counters read
+ *
+ * @param[in] p
+ *   Pipeline handle.
+ * @param[in] learner_name
+ *   Learner table name.
+ * @param[out] stats
+ *   Learner table stats. Must point to a pre-allocated structure. The *n_pkts_action* field also
+ *   needs to be pre-allocated as array of pipeline *n_actions* elements. The pipeline actions that
+ *   are not valid for the current learner table have their associated *n_pkts_action* element
+ *   always set to zero.
+ * @return
+ *   0 on success or the following error codes otherwise:
+ *   -EINVAL: Invalid argument.
+ */
+__rte_experimental
+int
+rte_swx_ctl_pipeline_learner_stats_read(struct rte_swx_pipeline *p,
+				      const char *learner_name,
+				      struct rte_swx_learner_stats *stats);
+
 /*
  * Table Update API.
  */
@@ -761,6 +900,27 @@ rte_swx_ctl_pipeline_selector_group_member_delete(struct rte_swx_ctl_pipeline *c
 						  uint32_t group_id,
 						  uint32_t member_id);
 
+/**
+ * Pipeline learner table default entry add
+ *
+ * Schedule learner table default entry update as part of the next commit operation.
+ *
+ * @param[in] ctl
+ *   Pipeline control handle.
+ * @param[in] learner_name
+ *   Learner table name.
+ * @param[in] entry
+ *   The new table default entry. The *key* and *key_mask* entry fields are ignored.
+ * @return
+ *   0 on success or the following error codes otherwise:
+ *   -EINVAL: Invalid argument.
+ */
+__rte_experimental
+int
+rte_swx_ctl_pipeline_learner_default_entry_add(struct rte_swx_ctl_pipeline *ctl,
+					       const char *learner_name,
+					       struct rte_swx_table_entry *entry);
+
 /**
  * Pipeline commit
  *
@@ -819,6 +979,31 @@ rte_swx_ctl_pipeline_table_entry_read(struct rte_swx_ctl_pipeline *ctl,
 				      const char *string,
 				      int *is_blank_or_comment);
 
+/**
+ * Pipeline learner table default entry read
+ *
+ * Read learner table default entry from string.
+ *
+ * @param[in] ctl
+ *   Pipeline control handle.
+ * @param[in] learner_name
+ *   Learner table name.
+ * @param[in] string
+ *   String containing the learner table default entry.
+ * @param[out] is_blank_or_comment
+ *   On error, this argument provides an indication of whether *string* contains
+ *   an invalid table entry (set to zero) or a blank or comment line that should
+ *   typically be ignored (set to a non-zero value).
+ * @return
+ *   0 on success or the following error codes otherwise:
+ *   -EINVAL: Invalid argument.
+ */
+struct rte_swx_table_entry *
+rte_swx_ctl_pipeline_learner_default_entry_read(struct rte_swx_ctl_pipeline *ctl,
+						const char *learner_name,
+						const char *string,
+						int *is_blank_or_comment);
+
 /**
  * Pipeline table print to file
  *
diff --git a/lib/pipeline/rte_swx_pipeline.c b/lib/pipeline/rte_swx_pipeline.c
index 13028bcc6a..a85d80289d 100644
--- a/lib/pipeline/rte_swx_pipeline.c
+++ b/lib/pipeline/rte_swx_pipeline.c
@@ -16,6 +16,7 @@
 #include <rte_meter.h>
 
 #include <rte_swx_table_selector.h>
+#include <rte_swx_table_learner.h>
 
 #include "rte_swx_pipeline.h"
 #include "rte_swx_ctl.h"
@@ -511,6 +512,13 @@ enum instruction_type {
 	/* table TABLE */
 	INSTR_TABLE,
 	INSTR_SELECTOR,
+	INSTR_LEARNER,
+
+	/* learn LEARNER ACTION_NAME */
+	INSTR_LEARNER_LEARN,
+
+	/* forget */
+	INSTR_LEARNER_FORGET,
 
 	/* extern e.obj.func */
 	INSTR_EXTERN_OBJ,
@@ -636,6 +644,10 @@ struct instr_table {
 	uint8_t table_id;
 };
 
+struct instr_learn {
+	uint8_t action_id;
+};
+
 struct instr_extern_obj {
 	uint8_t ext_obj_id;
 	uint8_t func_id;
@@ -726,6 +738,7 @@ struct instruction {
 		struct instr_dma dma;
 		struct instr_dst_src alu;
 		struct instr_table table;
+		struct instr_learn learn;
 		struct instr_extern_obj ext_obj;
 		struct instr_extern_func ext_func;
 		struct instr_jmp jmp;
@@ -746,7 +759,7 @@ struct action {
 	TAILQ_ENTRY(action) node;
 	char name[RTE_SWX_NAME_SIZE];
 	struct struct_type *st;
-	int *args_endianness; /* 0 = Host Byte Order (HBO). */
+	int *args_endianness; /* 0 = Host Byte Order (HBO); 1 = Network Byte Order (NBO). */
 	struct instruction *instructions;
 	uint32_t n_instructions;
 	uint32_t id;
@@ -839,6 +852,47 @@ struct selector_statistics {
 	uint64_t n_pkts;
 };
 
+/*
+ * Learner table.
+ */
+struct learner {
+	TAILQ_ENTRY(learner) node;
+	char name[RTE_SWX_NAME_SIZE];
+
+	/* Match. */
+	struct field **fields;
+	uint32_t n_fields;
+	struct header *header;
+
+	/* Action. */
+	struct action **actions;
+	struct field **action_arg;
+	struct action *default_action;
+	uint8_t *default_action_data;
+	uint32_t n_actions;
+	int default_action_is_const;
+	uint32_t action_data_size_max;
+
+	uint32_t size;
+	uint32_t timeout;
+	uint32_t id;
+};
+
+TAILQ_HEAD(learner_tailq, learner);
+
+struct learner_runtime {
+	void *mailbox;
+	uint8_t **key;
+	uint8_t **action_data;
+};
+
+struct learner_statistics {
+	uint64_t n_pkts_hit[2]; /* 0 = Miss, 1 = Hit. */
+	uint64_t n_pkts_learn[2]; /* 0 = Learn OK, 1 = Learn error. */
+	uint64_t n_pkts_forget;
+	uint64_t *n_pkts_action;
+};
+
 /*
  * Register array.
  */
@@ -919,9 +973,12 @@ struct thread {
 	/* Tables. */
 	struct table_runtime *tables;
 	struct selector_runtime *selectors;
+	struct learner_runtime *learners;
 	struct rte_swx_table_state *table_state;
 	uint64_t action_id;
 	int hit; /* 0 = Miss, 1 = Hit. */
+	uint32_t learner_id;
+	uint64_t time;
 
 	/* Extern objects and functions. */
 	struct extern_obj_runtime *extern_objs;
@@ -1355,6 +1412,7 @@ struct rte_swx_pipeline {
 	struct table_type_tailq table_types;
 	struct table_tailq tables;
 	struct selector_tailq selectors;
+	struct learner_tailq learners;
 	struct regarray_tailq regarrays;
 	struct meter_profile_tailq meter_profiles;
 	struct metarray_tailq metarrays;
@@ -1365,6 +1423,7 @@ struct rte_swx_pipeline {
 	struct rte_swx_table_state *table_state;
 	struct table_statistics *table_stats;
 	struct selector_statistics *selector_stats;
+	struct learner_statistics *learner_stats;
 	struct regarray_runtime *regarray_runtime;
 	struct metarray_runtime *metarray_runtime;
 	struct instruction *instructions;
@@ -1378,6 +1437,7 @@ struct rte_swx_pipeline {
 	uint32_t n_actions;
 	uint32_t n_tables;
 	uint32_t n_selectors;
+	uint32_t n_learners;
 	uint32_t n_regarrays;
 	uint32_t n_metarrays;
 	uint32_t n_headers;
@@ -3625,6 +3685,9 @@ table_find(struct rte_swx_pipeline *p, const char *name);
 static struct selector *
 selector_find(struct rte_swx_pipeline *p, const char *name);
 
+static struct learner *
+learner_find(struct rte_swx_pipeline *p, const char *name);
+
 static int
 instr_table_translate(struct rte_swx_pipeline *p,
 		      struct action *action,
@@ -3635,6 +3698,7 @@ instr_table_translate(struct rte_swx_pipeline *p,
 {
 	struct table *t;
 	struct selector *s;
+	struct learner *l;
 
 	CHECK(!action, EINVAL);
 	CHECK(n_tokens == 2, EINVAL);
@@ -3653,6 +3717,13 @@ instr_table_translate(struct rte_swx_pipeline *p,
 		return 0;
 	}
 
+	l = learner_find(p, tokens[1]);
+	if (l) {
+		instr->type = INSTR_LEARNER;
+		instr->table.table_id = l->id;
+		return 0;
+	}
+
 	CHECK(0, EINVAL);
 }
 
@@ -3746,6 +3817,168 @@ instr_selector_exec(struct rte_swx_pipeline *p)
 	thread_ip_inc(p);
 }
 
+static inline void
+instr_learner_exec(struct rte_swx_pipeline *p)
+{
+	struct thread *t = &p->threads[p->thread_id];
+	struct instruction *ip = t->ip;
+	uint32_t learner_id = ip->table.table_id;
+	struct rte_swx_table_state *ts = &t->table_state[p->n_tables +
+		p->n_selectors + learner_id];
+	struct learner_runtime *l = &t->learners[learner_id];
+	struct learner_statistics *stats = &p->learner_stats[learner_id];
+	uint64_t action_id, n_pkts_hit, n_pkts_action, time;
+	uint8_t *action_data;
+	int done, hit;
+
+	/* Table. */
+	time = rte_get_tsc_cycles();
+
+	done = rte_swx_table_learner_lookup(ts->obj,
+					    l->mailbox,
+					    time,
+					    l->key,
+					    &action_id,
+					    &action_data,
+					    &hit);
+	if (!done) {
+		/* Thread. */
+		TRACE("[Thread %2u] learner %u (not finalized)\n",
+		      p->thread_id,
+		      learner_id);
+
+		thread_yield(p);
+		return;
+	}
+
+	action_id = hit ? action_id : ts->default_action_id;
+	action_data = hit ? action_data : ts->default_action_data;
+	n_pkts_hit = stats->n_pkts_hit[hit];
+	n_pkts_action = stats->n_pkts_action[action_id];
+
+	TRACE("[Thread %2u] learner %u (%s, action %u)\n",
+	      p->thread_id,
+	      learner_id,
+	      hit ? "hit" : "miss",
+	      (uint32_t)action_id);
+
+	t->action_id = action_id;
+	t->structs[0] = action_data;
+	t->hit = hit;
+	t->learner_id = learner_id;
+	t->time = time;
+	stats->n_pkts_hit[hit] = n_pkts_hit + 1;
+	stats->n_pkts_action[action_id] = n_pkts_action + 1;
+
+	/* Thread. */
+	thread_ip_action_call(p, t, action_id);
+}
+
+/*
+ * learn.
+ */
+static struct action *
+action_find(struct rte_swx_pipeline *p, const char *name);
+
+static int
+action_has_nbo_args(struct action *a);
+
+static int
+instr_learn_translate(struct rte_swx_pipeline *p,
+		      struct action *action,
+		      char **tokens,
+		      int n_tokens,
+		      struct instruction *instr,
+		      struct instruction_data *data __rte_unused)
+{
+	struct action *a;
+
+	CHECK(action, EINVAL);
+	CHECK(n_tokens == 2, EINVAL);
+
+	a = action_find(p, tokens[1]);
+	CHECK(a, EINVAL);
+	CHECK(!action_has_nbo_args(a), EINVAL);
+
+	instr->type = INSTR_LEARNER_LEARN;
+	instr->learn.action_id = a->id;
+
+	return 0;
+}
+
+static inline void
+instr_learn_exec(struct rte_swx_pipeline *p)
+{
+	struct thread *t = &p->threads[p->thread_id];
+	struct instruction *ip = t->ip;
+	uint64_t action_id = ip->learn.action_id;
+	uint32_t learner_id = t->learner_id;
+	struct rte_swx_table_state *ts = &t->table_state[p->n_tables +
+		p->n_selectors + learner_id];
+	struct learner_runtime *l = &t->learners[learner_id];
+	struct learner_statistics *stats = &p->learner_stats[learner_id];
+	uint32_t status;
+
+	/* Table. */
+	status = rte_swx_table_learner_add(ts->obj,
+					   l->mailbox,
+					   t->time,
+					   action_id,
+					   l->action_data[action_id]);
+
+	TRACE("[Thread %2u] learner %u learn %s\n",
+	      p->thread_id,
+	      learner_id,
+	      status ? "ok" : "error");
+
+	stats->n_pkts_learn[status] += 1;
+
+	/* Thread. */
+	thread_ip_inc(p);
+}
+
+/*
+ * forget.
+ */
+static int
+instr_forget_translate(struct rte_swx_pipeline *p __rte_unused,
+		       struct action *action,
+		       char **tokens __rte_unused,
+		       int n_tokens,
+		       struct instruction *instr,
+		       struct instruction_data *data __rte_unused)
+{
+	CHECK(action, EINVAL);
+	CHECK(n_tokens == 1, EINVAL);
+
+	instr->type = INSTR_LEARNER_FORGET;
+
+	return 0;
+}
+
+static inline void
+instr_forget_exec(struct rte_swx_pipeline *p)
+{
+	struct thread *t = &p->threads[p->thread_id];
+	uint32_t learner_id = t->learner_id;
+	struct rte_swx_table_state *ts = &t->table_state[p->n_tables +
+		p->n_selectors + learner_id];
+	struct learner_runtime *l = &t->learners[learner_id];
+	struct learner_statistics *stats = &p->learner_stats[learner_id];
+
+	/* Table. */
+	rte_swx_table_learner_delete(ts->obj, l->mailbox);
+
+	TRACE("[Thread %2u] learner %u forget\n",
+	      p->thread_id,
+	      learner_id);
+
+	stats->n_pkts_forget += 1;
+
+	/* Thread. */
+	thread_ip_inc(p);
+}
+
 /*
  * extern.
  */
@@ -7159,9 +7392,6 @@ instr_meter_imi_exec(struct rte_swx_pipeline *p)
 /*
  * jmp.
  */
-static struct action *
-action_find(struct rte_swx_pipeline *p, const char *name);
-
 static int
 instr_jmp_translate(struct rte_swx_pipeline *p __rte_unused,
 		    struct action *action __rte_unused,
@@ -8136,6 +8366,22 @@ instr_translate(struct rte_swx_pipeline *p,
 					     instr,
 					     data);
 
+	if (!strcmp(tokens[tpos], "learn"))
+		return instr_learn_translate(p,
+					     action,
+					     &tokens[tpos],
+					     n_tokens - tpos,
+					     instr,
+					     data);
+
+	if (!strcmp(tokens[tpos], "forget"))
+		return instr_forget_translate(p,
+					      action,
+					      &tokens[tpos],
+					      n_tokens - tpos,
+					      instr,
+					      data);
+
 	if (!strcmp(tokens[tpos], "extern"))
 		return instr_extern_translate(p,
 					      action,
@@ -9096,6 +9342,9 @@ static instr_exec_t instruction_table[] = {
 
 	[INSTR_TABLE] = instr_table_exec,
 	[INSTR_SELECTOR] = instr_selector_exec,
+	[INSTR_LEARNER] = instr_learner_exec,
+	[INSTR_LEARNER_LEARN] = instr_learn_exec,
+	[INSTR_LEARNER_FORGET] = instr_forget_exec,
 	[INSTR_EXTERN_OBJ] = instr_extern_obj_exec,
 	[INSTR_EXTERN_FUNC] = instr_extern_func_exec,
 
@@ -9191,6 +9440,42 @@ action_field_parse(struct action *action, const char *name)
 	return action_field_find(action, &name[2]);
 }
 
+static int
+action_has_nbo_args(struct action *a)
+{
+	uint32_t i;
+
+	/* Return if the action does not have any args. */
+	if (!a->st)
+		return 0; /* FALSE */
+
+	for (i = 0; i < a->st->n_fields; i++)
+		if (a->args_endianness[i])
+			return 1; /* TRUE */
+
+	return 0; /* FALSE */
+}
+
+static int
+action_does_learning(struct action *a)
+{
+	uint32_t i;
+
+	for (i = 0; i < a->n_instructions; i++)
+		switch (a->instructions[i].type) {
+		case INSTR_LEARNER_LEARN:
+			return 1; /* TRUE */
+
+		case INSTR_LEARNER_FORGET:
+			return 1; /* TRUE */
+
+		default:
+			continue;
+		}
+
+	return 0; /* FALSE */
+}
+
 int
 rte_swx_pipeline_action_config(struct rte_swx_pipeline *p,
 			       const char *name,
@@ -9546,6 +9831,7 @@ rte_swx_pipeline_table_config(struct rte_swx_pipeline *p,
 	CHECK_NAME(name, EINVAL);
 	CHECK(!table_find(p, name), EEXIST);
 	CHECK(!selector_find(p, name), EEXIST);
+	CHECK(!learner_find(p, name), EEXIST);
 
 	CHECK(params, EINVAL);
 
@@ -9566,6 +9852,7 @@ rte_swx_pipeline_table_config(struct rte_swx_pipeline *p,
 
 		a = action_find(p, action_name);
 		CHECK(a, EINVAL);
+		CHECK(!action_does_learning(a), EINVAL);
 
 		action_data_size = a->st ? a->st->n_bits / 8 : 0;
 		if (action_data_size > action_data_size_max)
@@ -9964,6 +10251,7 @@ rte_swx_pipeline_selector_config(struct rte_swx_pipeline *p,
 	CHECK_NAME(name, EINVAL);
 	CHECK(!table_find(p, name), EEXIST);
 	CHECK(!selector_find(p, name), EEXIST);
+	CHECK(!learner_find(p, name), EEXIST);
 
 	CHECK(params, EINVAL);
 
@@ -10221,73 +10509,604 @@ selector_free(struct rte_swx_pipeline *p)
 }
 
 /*
- * Table state.
+ * Learner table.
  */
-static int
-table_state_build(struct rte_swx_pipeline *p)
+static struct learner *
+learner_find(struct rte_swx_pipeline *p, const char *name)
 {
-	struct table *table;
-	struct selector *s;
-
-	p->table_state = calloc(p->n_tables + p->n_selectors,
-				sizeof(struct rte_swx_table_state));
-	CHECK(p->table_state, ENOMEM);
+	struct learner *l;
 
-	TAILQ_FOREACH(table, &p->tables, node) {
-		struct rte_swx_table_state *ts = &p->table_state[table->id];
+	TAILQ_FOREACH(l, &p->learners, node)
+		if (!strcmp(l->name, name))
+			return l;
 
-		if (table->type) {
-			struct rte_swx_table_params *params;
+	return NULL;
+}
 
-			/* ts->obj. */
-			params = table_params_get(table);
-			CHECK(params, ENOMEM);
+static struct learner *
+learner_find_by_id(struct rte_swx_pipeline *p, uint32_t id)
+{
+	struct learner *l = NULL;
 
-			ts->obj = table->type->ops.create(params,
-				NULL,
-				table->args,
-				p->numa_node);
+	TAILQ_FOREACH(l, &p->learners, node)
+		if (l->id == id)
+			return l;
 
-			table_params_free(params);
-			CHECK(ts->obj, ENODEV);
-		}
+	return NULL;
+}
 
-		/* ts->default_action_data. */
-		if (table->action_data_size_max) {
-			ts->default_action_data =
-				malloc(table->action_data_size_max);
-			CHECK(ts->default_action_data, ENOMEM);
+static int
+learner_match_fields_check(struct rte_swx_pipeline *p,
+			   struct rte_swx_pipeline_learner_params *params,
+			   struct header **header)
+{
+	struct header *h0 = NULL;
+	struct field *hf, *mf;
+	uint32_t i;
 
-			memcpy(ts->default_action_data,
-			       table->default_action_data,
-			       table->action_data_size_max);
-		}
+	/* Return if no match fields. */
+	if (!params->n_fields || !params->field_names)
+		return -EINVAL;
 
-		/* ts->default_action_id. */
-		ts->default_action_id = table->default_action->id;
-	}
+	/* Check that all the match fields either belong to the same header
+	 * or are all meta-data fields.
+	 */
+	hf = header_field_parse(p, params->field_names[0], &h0);
+	mf = metadata_field_parse(p, params->field_names[0]);
+	if (!hf && !mf)
+		return -EINVAL;
 
-	TAILQ_FOREACH(s, &p->selectors, node) {
-		struct rte_swx_table_state *ts = &p->table_state[p->n_tables + s->id];
-		struct rte_swx_table_selector_params *params;
+	for (i = 1; i < params->n_fields; i++)
+		if (h0) {
+			struct header *h;
 
-		/* ts->obj. */
-		params = selector_table_params_get(s);
-		CHECK(params, ENOMEM);
+			hf = header_field_parse(p, params->field_names[i], &h);
+			if (!hf || (h->id != h0->id))
+				return -EINVAL;
+		} else {
+			mf = metadata_field_parse(p, params->field_names[i]);
+			if (!mf)
+				return -EINVAL;
+		}
 
-		ts->obj = rte_swx_table_selector_create(params, NULL, p->numa_node);
+	/* Check that there are no duplicated match fields. */
+	for (i = 0; i < params->n_fields; i++) {
+		const char *field_name = params->field_names[i];
+		uint32_t j;
 
-		selector_params_free(params);
-		CHECK(ts->obj, ENODEV);
+		for (j = i + 1; j < params->n_fields; j++)
+			if (!strcmp(params->field_names[j], field_name))
+				return -EINVAL;
 	}
 
+	/* Return. */
+	if (header)
+		*header = h0;
+
 	return 0;
 }
 
-static void
-table_state_build_free(struct rte_swx_pipeline *p)
+static int
+learner_action_args_check(struct rte_swx_pipeline *p, struct action *a, const char *mf_name)
 {
-	uint32_t i;
+	struct struct_type *mst = p->metadata_st, *ast = a->st;
+	struct field *mf, *af;
+	uint32_t mf_pos, i;
+
+	if (!ast) {
+		if (mf_name)
+			return -EINVAL;
+
+		return 0;
+	}
+
+	/* Check that mf_name is the name of a valid meta-data field. */
+	CHECK_NAME(mf_name, EINVAL);
+	mf = metadata_field_parse(p, mf_name);
+	CHECK(mf, EINVAL);
+
+	/* Check that there are enough meta-data fields, starting with the mf_name field, to cover
+	 * all the action arguments.
+	 */
+	mf_pos = mf - mst->fields;
+	CHECK(mst->n_fields - mf_pos >= ast->n_fields, EINVAL);
+
+	/* Check that the size of each of the identified meta-data fields matches exactly the size
+	 * of the corresponding action argument.
+	 */
+	for (i = 0; i < ast->n_fields; i++) {
+		mf = &mst->fields[mf_pos + i];
+		af = &ast->fields[i];
+
+		CHECK(mf->n_bits == af->n_bits, EINVAL);
+	}
+
+	return 0;
+}
+
+static int
+learner_action_learning_check(struct rte_swx_pipeline *p,
+			      struct action *action,
+			      const char **action_names,
+			      uint32_t n_actions)
+{
+	uint32_t i;
+
+	/* For each "learn" instruction of the current action, check that the learned action (i.e.
+	 * the action passed as argument to the "learn" instruction) is also enabled for the
+	 * current learner table.
+	 */
+	for (i = 0; i < action->n_instructions; i++) {
+		struct instruction *instr = &action->instructions[i];
+		uint32_t found = 0, j;
+
+		if (instr->type != INSTR_LEARNER_LEARN)
+			continue;
+
+		for (j = 0; j < n_actions; j++) {
+			struct action *a;
+
+			a = action_find(p, action_names[j]);
+			if (!a)
+				return -EINVAL;
+
+			if (a->id == instr->learn.action_id)
+				found = 1;
+		}
+
+		if (!found)
+			return -EINVAL;
+	}
+
+	return 0;
+}
+
+int
+rte_swx_pipeline_learner_config(struct rte_swx_pipeline *p,
+			      const char *name,
+			      struct rte_swx_pipeline_learner_params *params,
+			      uint32_t size,
+			      uint32_t timeout)
+{
+	struct learner *l = NULL;
+	struct action *default_action;
+	struct header *header = NULL;
+	uint32_t action_data_size_max = 0, i;
+	int status = 0;
+
+	CHECK(p, EINVAL);
+
+	CHECK_NAME(name, EINVAL);
+	CHECK(!table_find(p, name), EEXIST);
+	CHECK(!selector_find(p, name), EEXIST);
+	CHECK(!learner_find(p, name), EEXIST);
+
+	CHECK(params, EINVAL);
+
+	/* Match checks. */
+	status = learner_match_fields_check(p, params, &header);
+	if (status)
+		return status;
+
+	/* Action checks. */
+	CHECK(params->n_actions, EINVAL);
+
+	CHECK(params->action_names, EINVAL);
+	for (i = 0; i < params->n_actions; i++) {
+		const char *action_name = params->action_names[i];
+		const char *action_field_name = params->action_field_names[i];
+		struct action *a;
+		uint32_t action_data_size;
+
+		CHECK_NAME(action_name, EINVAL);
+
+		a = action_find(p, action_name);
+		CHECK(a, EINVAL);
+
+		status = learner_action_args_check(p, a, action_field_name);
+		if (status)
+			return status;
+
+		status = learner_action_learning_check(p,
+						       a,
+						       params->action_names,
+						       params->n_actions);
+		if (status)
+			return status;
+
+		action_data_size = a->st ? a->st->n_bits / 8 : 0;
+		if (action_data_size > action_data_size_max)
+			action_data_size_max = action_data_size;
+	}
+
+	CHECK_NAME(params->default_action_name, EINVAL);
+	for (i = 0; i < p->n_actions; i++)
+		if (!strcmp(params->action_names[i],
+			    params->default_action_name))
+			break;
+	CHECK(i < params->n_actions, EINVAL);
+
+	default_action = action_find(p, params->default_action_name);
+	CHECK((default_action->st && params->default_action_data) ||
+	      !params->default_action_data, EINVAL);
+
+	/* Any other checks. */
+	CHECK(size, EINVAL);
+	CHECK(timeout, EINVAL);
+
+	/* Memory allocation. */
+	l = calloc(1, sizeof(struct learner));
+	if (!l)
+		goto nomem;
+
+	l->fields = calloc(params->n_fields, sizeof(struct field *));
+	if (!l->fields)
+		goto nomem;
+
+	l->actions = calloc(params->n_actions, sizeof(struct action *));
+	if (!l->actions)
+		goto nomem;
+
+	l->action_arg = calloc(params->n_actions, sizeof(struct field *));
+	if (!l->action_arg)
+		goto nomem;
+
+	if (action_data_size_max) {
+		l->default_action_data = calloc(1, action_data_size_max);
+		if (!l->default_action_data)
+			goto nomem;
+	}
+
+	/* Node initialization. */
+	strcpy(l->name, name);
+
+	for (i = 0; i < params->n_fields; i++) {
+		const char *field_name = params->field_names[i];
+
+		l->fields[i] = header ?
+			header_field_parse(p, field_name, NULL) :
+			metadata_field_parse(p, field_name);
+	}
+
+	l->n_fields = params->n_fields;
+
+	l->header = header;
+
+	for (i = 0; i < params->n_actions; i++) {
+		const char *mf_name = params->action_field_names[i];
+
+		l->actions[i] = action_find(p, params->action_names[i]);
+
+		l->action_arg[i] = mf_name ? metadata_field_parse(p, mf_name) : NULL;
+	}
+
+	l->default_action = default_action;
+
+	if (default_action->st)
+		memcpy(l->default_action_data,
+		       params->default_action_data,
+		       default_action->st->n_bits / 8);
+
+	l->n_actions = params->n_actions;
+
+	l->default_action_is_const = params->default_action_is_const;
+
+	l->action_data_size_max = action_data_size_max;
+
+	l->size = size;
+
+	l->timeout = timeout;
+
+	l->id = p->n_learners;
+
+	/* Node add to tailq. */
+	TAILQ_INSERT_TAIL(&p->learners, l, node);
+	p->n_learners++;
+
+	return 0;
+
+nomem:
+	if (!l)
+		return -ENOMEM;
+
+	free(l->action_arg);
+	free(l->actions);
+	free(l->fields);
+	free(l);
+
+	return -ENOMEM;
+}
+
+static void
+learner_params_free(struct rte_swx_table_learner_params *params)
+{
+	if (!params)
+		return;
+
+	free(params->key_mask0);
+
+	free(params);
+}
+
+static struct rte_swx_table_learner_params *
+learner_params_get(struct learner *l)
+{
+	struct rte_swx_table_learner_params *params = NULL;
+	struct field *first, *last;
+	uint32_t i;
+
+	/* Memory allocation. */
+	params = calloc(1, sizeof(struct rte_swx_table_learner_params));
+	if (!params)
+		goto error;
+
+	/* Find first (smallest offset) and last (biggest offset) match fields. */
+	first = l->fields[0];
+	last = l->fields[0];
+
+	for (i = 0; i < l->n_fields; i++) {
+		struct field *f = l->fields[i];
+
+		if (f->offset < first->offset)
+			first = f;
+
+		if (f->offset > last->offset)
+			last = f;
+	}
+
+	/* Key offset and size. */
+	params->key_offset = first->offset / 8;
+	params->key_size = (last->offset + last->n_bits - first->offset) / 8;
+
+	/* Memory allocation. */
+	params->key_mask0 = calloc(1, params->key_size);
+	if (!params->key_mask0)
+		goto error;
+
+	/* Key mask. */
+	for (i = 0; i < l->n_fields; i++) {
+		struct field *f = l->fields[i];
+		uint32_t start = (f->offset - first->offset) / 8;
+		size_t size = f->n_bits / 8;
+
+		memset(&params->key_mask0[start], 0xFF, size);
+	}
+
+	/* Action data size. */
+	params->action_data_size = l->action_data_size_max;
+
+	/* Maximum number of keys. */
+	params->n_keys_max = l->size;
+
+	/* Timeout. */
+	params->key_timeout = l->timeout;
+
+	return params;
+
+error:
+	learner_params_free(params);
+	return NULL;
+}
+
+static void
+learner_build_free(struct rte_swx_pipeline *p)
+{
+	uint32_t i;
+
+	for (i = 0; i < RTE_SWX_PIPELINE_THREADS_MAX; i++) {
+		struct thread *t = &p->threads[i];
+		uint32_t j;
+
+		if (!t->learners)
+			continue;
+
+		for (j = 0; j < p->n_learners; j++) {
+			struct learner_runtime *r = &t->learners[j];
+
+			free(r->mailbox);
+			free(r->action_data);
+		}
+
+		free(t->learners);
+		t->learners = NULL;
+	}
+
+	if (p->learner_stats) {
+		for (i = 0; i < p->n_learners; i++)
+			free(p->learner_stats[i].n_pkts_action);
+
+		free(p->learner_stats);
+	}
+}
+
+static int
+learner_build(struct rte_swx_pipeline *p)
+{
+	uint32_t i;
+	int status = 0;
+
+	/* Per pipeline: learner statistics. */
+	p->learner_stats = calloc(p->n_learners, sizeof(struct learner_statistics));
+	CHECK(p->learner_stats, ENOMEM);
+
+	for (i = 0; i < p->n_learners; i++) {
+		p->learner_stats[i].n_pkts_action = calloc(p->n_actions, sizeof(uint64_t));
+		CHECK(p->learner_stats[i].n_pkts_action, ENOMEM);
+	}
+
+	/* Per thread: learner run-time. */
+	for (i = 0; i < RTE_SWX_PIPELINE_THREADS_MAX; i++) {
+		struct thread *t = &p->threads[i];
+		struct learner *l;
+
+		t->learners = calloc(p->n_learners, sizeof(struct learner_runtime));
+		if (!t->learners) {
+			status = -ENOMEM;
+			goto error;
+		}
+
+		TAILQ_FOREACH(l, &p->learners, node) {
+			struct learner_runtime *r = &t->learners[l->id];
+			uint64_t size;
+			uint32_t j;
+
+			/* r->mailbox. */
+			size = rte_swx_table_learner_mailbox_size_get();
+			if (size) {
+				r->mailbox = calloc(1, size);
+				if (!r->mailbox) {
+					status = -ENOMEM;
+					goto error;
+				}
+			}
+
+			/* r->key. */
+			r->key = l->header ?
+				&t->structs[l->header->struct_id] :
+				&t->structs[p->metadata_struct_id];
+
+			/* r->action_data. */
+			r->action_data = calloc(p->n_actions, sizeof(uint8_t *));
+			if (!r->action_data) {
+				status = -ENOMEM;
+				goto error;
+			}
+
+			for (j = 0; j < l->n_actions; j++) {
+				struct action *a = l->actions[j];
+				struct field *mf = l->action_arg[j];
+				uint8_t *m = t->structs[p->metadata_struct_id];
+
+				r->action_data[a->id] = mf ? &m[mf->offset / 8] : NULL;
+			}
+		}
+	}
+
+	return 0;
+
+error:
+	learner_build_free(p);
+	return status;
+}
+
+static void
+learner_free(struct rte_swx_pipeline *p)
+{
+	learner_build_free(p);
+
+	/* Learner tables. */
+	for ( ; ; ) {
+		struct learner *l;
+
+		l = TAILQ_FIRST(&p->learners);
+		if (!l)
+			break;
+
+		TAILQ_REMOVE(&p->learners, l, node);
+		free(l->fields);
+		free(l->actions);
+		free(l->action_arg);
+		free(l->default_action_data);
+		free(l);
+	}
+}
+
+/*
+ * Table state.
+ */
+static int
+table_state_build(struct rte_swx_pipeline *p)
+{
+	struct table *table;
+	struct selector *s;
+	struct learner *l;
+
+	p->table_state = calloc(p->n_tables + p->n_selectors,
+				sizeof(struct rte_swx_table_state));
+	CHECK(p->table_state, ENOMEM);
+
+	TAILQ_FOREACH(table, &p->tables, node) {
+		struct rte_swx_table_state *ts = &p->table_state[table->id];
+
+		if (table->type) {
+			struct rte_swx_table_params *params;
+
+			/* ts->obj. */
+			params = table_params_get(table);
+			CHECK(params, ENOMEM);
+
+			ts->obj = table->type->ops.create(params,
+				NULL,
+				table->args,
+				p->numa_node);
+
+			table_params_free(params);
+			CHECK(ts->obj, ENODEV);
+		}
+
+		/* ts->default_action_data. */
+		if (table->action_data_size_max) {
+			ts->default_action_data =
+				malloc(table->action_data_size_max);
+			CHECK(ts->default_action_data, ENOMEM);
+
+			memcpy(ts->default_action_data,
+			       table->default_action_data,
+			       table->action_data_size_max);
+		}
+
+		/* ts->default_action_id. */
+		ts->default_action_id = table->default_action->id;
+	}
+
+	TAILQ_FOREACH(s, &p->selectors, node) {
+		struct rte_swx_table_state *ts = &p->table_state[p->n_tables + s->id];
+		struct rte_swx_table_selector_params *params;
+
+		/* ts->obj. */
+		params = selector_table_params_get(s);
+		CHECK(params, ENOMEM);
+
+		ts->obj = rte_swx_table_selector_create(params, NULL, p->numa_node);
+
+		selector_params_free(params);
+		CHECK(ts->obj, ENODEV);
+	}
+
+	TAILQ_FOREACH(l, &p->learners, node) {
+		struct rte_swx_table_state *ts = &p->table_state[p->n_tables +
+			p->n_selectors + l->id];
+		struct rte_swx_table_learner_params *params;
+
+		/* ts->obj. */
+		params = learner_params_get(l);
+		CHECK(params, ENOMEM);
+
+		ts->obj = rte_swx_table_learner_create(params, p->numa_node);
+		learner_params_free(params);
+		CHECK(ts->obj, ENODEV);
+
+		/* ts->default_action_data. */
+		if (l->action_data_size_max) {
+			ts->default_action_data = malloc(l->action_data_size_max);
+			CHECK(ts->default_action_data, ENOMEM);
+
+			memcpy(ts->default_action_data,
+			       l->default_action_data,
+			       l->action_data_size_max);
+		}
+
+		/* ts->default_action_id. */
+		ts->default_action_id = l->default_action->id;
+	}
+
+	return 0;
+}
+
+static void
+table_state_build_free(struct rte_swx_pipeline *p)
+{
+	uint32_t i;
 
 	if (!p->table_state)
 		return;
@@ -10312,6 +11131,17 @@ table_state_build_free(struct rte_swx_pipeline *p)
 			rte_swx_table_selector_free(ts->obj);
 	}
 
+	for (i = 0; i < p->n_learners; i++) {
+		struct rte_swx_table_state *ts = &p->table_state[p->n_tables + p->n_selectors + i];
+
+		/* ts->obj. */
+		if (ts->obj)
+			rte_swx_table_learner_free(ts->obj);
+
+		/* ts->default_action_data. */
+		free(ts->default_action_data);
+	}
+
 	free(p->table_state);
 	p->table_state = NULL;
 }
@@ -10653,6 +11483,7 @@ rte_swx_pipeline_config(struct rte_swx_pipeline **p, int numa_node)
 	TAILQ_INIT(&pipeline->table_types);
 	TAILQ_INIT(&pipeline->tables);
 	TAILQ_INIT(&pipeline->selectors);
+	TAILQ_INIT(&pipeline->learners);
 	TAILQ_INIT(&pipeline->regarrays);
 	TAILQ_INIT(&pipeline->meter_profiles);
 	TAILQ_INIT(&pipeline->metarrays);
@@ -10675,6 +11506,7 @@ rte_swx_pipeline_free(struct rte_swx_pipeline *p)
 	metarray_free(p);
 	regarray_free(p);
 	table_state_free(p);
+	learner_free(p);
 	selector_free(p);
 	table_free(p);
 	action_free(p);
@@ -10759,6 +11591,10 @@ rte_swx_pipeline_build(struct rte_swx_pipeline *p)
 	if (status)
 		goto error;
 
+	status = learner_build(p);
+	if (status)
+		goto error;
+
 	status = table_state_build(p);
 	if (status)
 		goto error;
@@ -10778,6 +11614,7 @@ rte_swx_pipeline_build(struct rte_swx_pipeline *p)
 	metarray_build_free(p);
 	regarray_build_free(p);
 	table_state_build_free(p);
+	learner_build_free(p);
 	selector_build_free(p);
 	table_build_free(p);
 	action_build_free(p);
@@ -10839,6 +11676,7 @@ rte_swx_ctl_pipeline_info_get(struct rte_swx_pipeline *p,
 	pipeline->n_actions = n_actions;
 	pipeline->n_tables = n_tables;
 	pipeline->n_selectors = p->n_selectors;
+	pipeline->n_learners = p->n_learners;
 	pipeline->n_regarrays = p->n_regarrays;
 	pipeline->n_metarrays = p->n_metarrays;
 
@@ -11084,6 +11922,75 @@ rte_swx_ctl_selector_member_id_field_info_get(struct rte_swx_pipeline *p,
 	return 0;
 }
 
+int
+rte_swx_ctl_learner_info_get(struct rte_swx_pipeline *p,
+			     uint32_t learner_id,
+			     struct rte_swx_ctl_learner_info *learner)
+{
+	struct learner *l = NULL;
+
+	if (!p || !learner)
+		return -EINVAL;
+
+	l = learner_find_by_id(p, learner_id);
+	if (!l)
+		return -EINVAL;
+
+	strcpy(learner->name, l->name);
+
+	learner->n_match_fields = l->n_fields;
+	learner->n_actions = l->n_actions;
+	learner->default_action_is_const = l->default_action_is_const;
+	learner->size = l->size;
+
+	return 0;
+}
+
+int
+rte_swx_ctl_learner_match_field_info_get(struct rte_swx_pipeline *p,
+					 uint32_t learner_id,
+					 uint32_t match_field_id,
+					 struct rte_swx_ctl_table_match_field_info *match_field)
+{
+	struct learner *l;
+	struct field *f;
+
+	if (!p || (learner_id >= p->n_learners) || !match_field)
+		return -EINVAL;
+
+	l = learner_find_by_id(p, learner_id);
+	if (!l || (match_field_id >= l->n_fields))
+		return -EINVAL;
+
+	f = l->fields[match_field_id];
+	match_field->match_type = RTE_SWX_TABLE_MATCH_EXACT;
+	match_field->is_header = l->header ? 1 : 0;
+	match_field->n_bits = f->n_bits;
+	match_field->offset = f->offset;
+
+	return 0;
+}
+
+int
+rte_swx_ctl_learner_action_info_get(struct rte_swx_pipeline *p,
+				    uint32_t learner_id,
+				    uint32_t learner_action_id,
+				    struct rte_swx_ctl_table_action_info *learner_action)
+{
+	struct learner *l;
+
+	if (!p || (learner_id >= p->n_learners) || !learner_action)
+		return -EINVAL;
+
+	l = learner_find_by_id(p, learner_id);
+	if (!l || (learner_action_id >= l->n_actions))
+		return -EINVAL;
+
+	learner_action->action_id = l->actions[learner_action_id]->id;
+
+	return 0;
+}
+
 int
 rte_swx_pipeline_table_state_get(struct rte_swx_pipeline *p,
 				 struct rte_swx_table_state **table_state)
@@ -11188,6 +12095,38 @@ rte_swx_ctl_pipeline_selector_stats_read(struct rte_swx_pipeline *p,
 	return 0;
 }
 
+int
+rte_swx_ctl_pipeline_learner_stats_read(struct rte_swx_pipeline *p,
+					const char *learner_name,
+					struct rte_swx_learner_stats *stats)
+{
+	struct learner *l;
+	struct learner_statistics *learner_stats;
+
+	if (!p || !learner_name || !learner_name[0] || !stats || !stats->n_pkts_action)
+		return -EINVAL;
+
+	l = learner_find(p, learner_name);
+	if (!l)
+		return -EINVAL;
+
+	learner_stats = &p->learner_stats[l->id];
+
+	memcpy(&stats->n_pkts_action,
+	       &learner_stats->n_pkts_action,
+	       p->n_actions * sizeof(uint64_t));
+
+	stats->n_pkts_hit = learner_stats->n_pkts_hit[1];
+	stats->n_pkts_miss = learner_stats->n_pkts_hit[0];
+
+	stats->n_pkts_learn_ok = learner_stats->n_pkts_learn[0];
+	stats->n_pkts_learn_err = learner_stats->n_pkts_learn[1];
+
+	stats->n_pkts_forget = learner_stats->n_pkts_forget;
+
+	return 0;
+}
+
 int
 rte_swx_ctl_regarray_info_get(struct rte_swx_pipeline *p,
 			      uint32_t regarray_id,
diff --git a/lib/pipeline/rte_swx_pipeline.h b/lib/pipeline/rte_swx_pipeline.h
index 5afca2bc20..2f18a820b9 100644
--- a/lib/pipeline/rte_swx_pipeline.h
+++ b/lib/pipeline/rte_swx_pipeline.h
@@ -676,6 +676,83 @@ rte_swx_pipeline_selector_config(struct rte_swx_pipeline *p,
 				 const char *name,
 				 struct rte_swx_pipeline_selector_params *params);
 
+/** Pipeline learner table parameters. */
+struct rte_swx_pipeline_learner_params {
+	/** The set of match fields for the current table.
+	 * Restriction: All the match fields of the current table need to be
+	 * part of the same struct, i.e. either all the match fields are part of
+	 * the same header or all the match fields are part of the meta-data.
+	 */
+	const char **field_names;
+
+	/** The number of match fields for the current table. Must be non-zero.
+	 */
+	uint32_t n_fields;
+
+	/** The set of actions for the current table. */
+	const char **action_names;
+
+	/** The number of actions for the current table. Must be at least one.
+	 */
+	uint32_t n_actions;
+
+	/** This table type allows adding the latest lookup key (typically done
+	 * only in the case of lookup miss) to the table with a given action.
+	 * The action arguments are picked up from the packet meta-data: for
+	 * each action, a set of successive meta-data fields (with the name of
+	 * the first such field provided here) is 1:1 mapped to the action
+	 * arguments. These meta-data fields must be set with the actual values
+	 * of the action arguments before the key add operation.
+	 */
+	const char **action_field_names;
+
+	/** The default table action that gets executed on lookup miss. Must be
+	 * one of the table actions included in the *action_names*.
+	 */
+	const char *default_action_name;
+
+	/** Default action data. The size of this array is the action data size
+	 * of the default action. Must be NULL if the default action data size
+	 * is zero.
+	 */
+	uint8_t *default_action_data;
+
+	/** If non-zero (true), then the default action of the current table
+	 * cannot be changed. If zero (false), then the default action can be
+	 * changed in the future with another action from the *action_names*
+	 * list.
+	 */
+	int default_action_is_const;
+};
+
+/**
+ * Pipeline learner table configure
+ *
+ * @param[out] p
+ *   Pipeline handle.
+ * @param[in] name
+ *   Learner table name.
+ * @param[in] params
+ *   Learner table parameters.
+ * @param[in] size
+ *   The maximum number of table entries. Must be non-zero.
+ * @param[in] timeout
+ *   Table entry timeout in seconds. Must be non-zero.
+ * @return
+ *   0 on success or the following error codes otherwise:
+ *   -EINVAL: Invalid argument;
+ *   -ENOMEM: Not enough space/cannot allocate memory;
+ *   -EEXIST: Learner table with this name already exists;
+ *   -ENODEV: Learner table creation error.
+ */
+__rte_experimental
+int
+rte_swx_pipeline_learner_config(struct rte_swx_pipeline *p,
+				const char *name,
+				struct rte_swx_pipeline_learner_params *params,
+				uint32_t size,
+				uint32_t timeout);
+
 /**
  * Pipeline register array configure
  *
diff --git a/lib/pipeline/rte_swx_pipeline_spec.c b/lib/pipeline/rte_swx_pipeline_spec.c
index c57893f18c..d9cd1d0595 100644
--- a/lib/pipeline/rte_swx_pipeline_spec.c
+++ b/lib/pipeline/rte_swx_pipeline_spec.c
@@ -20,7 +20,10 @@
 #define TABLE_ACTIONS_BLOCK 4
 #define SELECTOR_BLOCK 5
 #define SELECTOR_SELECTOR_BLOCK 6
-#define APPLY_BLOCK 7
+#define LEARNER_BLOCK 7
+#define LEARNER_KEY_BLOCK 8
+#define LEARNER_ACTIONS_BLOCK 9
+#define APPLY_BLOCK 10
 
 /*
  * extobj.
@@ -1281,6 +1284,420 @@ selector_block_parse(struct selector_spec *s,
 	return -EINVAL;
 }
 
+/*
+ * learner.
+ *
+ * learner {
+ *	key {
+ *		MATCH_FIELD_NAME
+ *		...
+ *	}
+ *	actions {
+ *		ACTION_NAME args METADATA_FIELD_NAME
+ *		...
+ *	}
+ *	default_action ACTION_NAME args none | ARGS_BYTE_ARRAY [ const ]
+ *	size SIZE
+ *	timeout TIMEOUT_IN_SECONDS
+ * }
+ */
+struct learner_spec {
+	char *name;
+	struct rte_swx_pipeline_learner_params params;
+	uint32_t size;
+	uint32_t timeout;
+};
+
+static void
+learner_spec_free(struct learner_spec *s)
+{
+	uintptr_t default_action_name;
+	uint32_t i;
+
+	if (!s)
+		return;
+
+	free(s->name);
+	s->name = NULL;
+
+	for (i = 0; i < s->params.n_fields; i++) {
+		uintptr_t name = (uintptr_t)s->params.field_names[i];
+
+		free((void *)name);
+	}
+
+	free(s->params.field_names);
+	s->params.field_names = NULL;
+
+	s->params.n_fields = 0;
+
+	for (i = 0; i < s->params.n_actions; i++) {
+		uintptr_t name = (uintptr_t)s->params.action_names[i];
+
+		free((void *)name);
+	}
+
+	free(s->params.action_names);
+	s->params.action_names = NULL;
+
+	for (i = 0; i < s->params.n_actions; i++) {
+		uintptr_t name = (uintptr_t)s->params.action_field_names[i];
+
+		free((void *)name);
+	}
+
+	free(s->params.action_field_names);
+	s->params.action_field_names = NULL;
+
+	s->params.n_actions = 0;
+
+	default_action_name = (uintptr_t)s->params.default_action_name;
+	free((void *)default_action_name);
+	s->params.default_action_name = NULL;
+
+	free(s->params.default_action_data);
+	s->params.default_action_data = NULL;
+
+	s->params.default_action_is_const = 0;
+
+	s->size = 0;
+
+	s->timeout = 0;
+}
+
+static int
+learner_key_statement_parse(uint32_t *block_mask,
+			    char **tokens,
+			    uint32_t n_tokens,
+			    uint32_t n_lines,
+			    uint32_t *err_line,
+			    const char **err_msg)
+{
+	/* Check format. */
+	if ((n_tokens != 2) || strcmp(tokens[1], "{")) {
+		if (err_line)
+			*err_line = n_lines;
+		if (err_msg)
+			*err_msg = "Invalid key statement.";
+		return -EINVAL;
+	}
+
+	/* block_mask. */
+	*block_mask |= 1 << LEARNER_KEY_BLOCK;
+
+	return 0;
+}
+
+static int
+learner_key_block_parse(struct learner_spec *s,
+			uint32_t *block_mask,
+			char **tokens,
+			uint32_t n_tokens,
+			uint32_t n_lines,
+			uint32_t *err_line,
+			const char **err_msg)
+{
+	const char **new_field_names = NULL;
+	char *field_name = NULL;
+
+	/* Handle end of block. */
+	if ((n_tokens == 1) && !strcmp(tokens[0], "}")) {
+		*block_mask &= ~(1 << LEARNER_KEY_BLOCK);
+		return 0;
+	}
+
+	/* Check input arguments. */
+	if (n_tokens != 1) {
+		if (err_line)
+			*err_line = n_lines;
+		if (err_msg)
+			*err_msg = "Invalid match field statement.";
+		return -EINVAL;
+	}
+
+	field_name = strdup(tokens[0]);
+	new_field_names = realloc(s->params.field_names, (s->params.n_fields + 1) * sizeof(char *));
+	if (!field_name || !new_field_names) {
+		free(field_name);
+		free(new_field_names);
+
+		if (err_line)
+			*err_line = n_lines;
+		if (err_msg)
+			*err_msg = "Memory allocation failed.";
+		return -ENOMEM;
+	}
+
+	s->params.field_names = new_field_names;
+	s->params.field_names[s->params.n_fields] = field_name;
+	s->params.n_fields++;
+
+	return 0;
+}
+
+static int
+learner_actions_statement_parse(uint32_t *block_mask,
+				char **tokens,
+				uint32_t n_tokens,
+				uint32_t n_lines,
+				uint32_t *err_line,
+				const char **err_msg)
+{
+	/* Check format. */
+	if ((n_tokens != 2) || strcmp(tokens[1], "{")) {
+		if (err_line)
+			*err_line = n_lines;
+		if (err_msg)
+			*err_msg = "Invalid actions statement.";
+		return -EINVAL;
+	}
+
+	/* block_mask. */
+	*block_mask |= 1 << LEARNER_ACTIONS_BLOCK;
+
+	return 0;
+}
+
+static int
+learner_actions_block_parse(struct learner_spec *s,
+			    uint32_t *block_mask,
+			    char **tokens,
+			    uint32_t n_tokens,
+			    uint32_t n_lines,
+			    uint32_t *err_line,
+			    const char **err_msg)
+{
+	const char **new_action_names = NULL;
+	const char **new_action_field_names = NULL;
+	char *action_name = NULL, *action_field_name = NULL;
+	int has_args = 1;
+
+	/* Handle end of block. */
+	if ((n_tokens == 1) && !strcmp(tokens[0], "}")) {
+		*block_mask &= ~(1 << LEARNER_ACTIONS_BLOCK);
+		return 0;
+	}
+
+	/* Check input arguments. */
+	if ((n_tokens != 3) || strcmp(tokens[1], "args")) {
+		if (err_line)
+			*err_line = n_lines;
+		if (err_msg)
+			*err_msg = "Invalid action name statement.";
+		return -EINVAL;
+	}
+
+	if (!strcmp(tokens[2], "none"))
+		has_args = 0;
+
+	action_name = strdup(tokens[0]);
+
+	if (has_args)
+		action_field_name = strdup(tokens[2]);
+
+	new_action_names = realloc(s->params.action_names,
+				   (s->params.n_actions + 1) * sizeof(char *));
+
+	new_action_field_names = realloc(s->params.action_field_names,
+					 (s->params.n_actions + 1) * sizeof(char *));
+
+	if (!action_name ||
+	    (has_args && !action_field_name) ||
+	    !new_action_names ||
+	    !new_action_field_names) {
+		free(action_name);
+		free(action_field_name);
+		free(new_action_names);
+		free(new_action_field_names);
+
+		if (err_line)
+			*err_line = n_lines;
+		if (err_msg)
+			*err_msg = "Memory allocation failed.";
+		return -ENOMEM;
+	}
+
+	s->params.action_names = new_action_names;
+	s->params.action_names[s->params.n_actions] = action_name;
+	s->params.action_field_names = new_action_field_names;
+	s->params.action_field_names[s->params.n_actions] = action_field_name;
+	s->params.n_actions++;
+
+	return 0;
+}
+
+static int
+learner_statement_parse(struct learner_spec *s,
+		      uint32_t *block_mask,
+		      char **tokens,
+		      uint32_t n_tokens,
+		      uint32_t n_lines,
+		      uint32_t *err_line,
+		      const char **err_msg)
+{
+	/* Check format. */
+	if ((n_tokens != 3) || strcmp(tokens[2], "{")) {
+		if (err_line)
+			*err_line = n_lines;
+		if (err_msg)
+			*err_msg = "Invalid learner statement.";
+		return -EINVAL;
+	}
+
+	/* spec. */
+	s->name = strdup(tokens[1]);
+	if (!s->name) {
+		if (err_line)
+			*err_line = n_lines;
+		if (err_msg)
+			*err_msg = "Memory allocation failed.";
+		return -ENOMEM;
+	}
+
+	/* block_mask. */
+	*block_mask |= 1 << LEARNER_BLOCK;
+
+	return 0;
+}
+
+static int
+learner_block_parse(struct learner_spec *s,
+		    uint32_t *block_mask,
+		    char **tokens,
+		    uint32_t n_tokens,
+		    uint32_t n_lines,
+		    uint32_t *err_line,
+		    const char **err_msg)
+{
+	if (*block_mask & (1 << LEARNER_KEY_BLOCK))
+		return learner_key_block_parse(s,
+					       block_mask,
+					       tokens,
+					       n_tokens,
+					       n_lines,
+					       err_line,
+					       err_msg);
+
+	if (*block_mask & (1 << LEARNER_ACTIONS_BLOCK))
+		return learner_actions_block_parse(s,
+						   block_mask,
+						   tokens,
+						   n_tokens,
+						   n_lines,
+						   err_line,
+						   err_msg);
+
+	/* Handle end of block. */
+	if ((n_tokens == 1) && !strcmp(tokens[0], "}")) {
+		*block_mask &= ~(1 << LEARNER_BLOCK);
+		return 0;
+	}
+
+	if (!strcmp(tokens[0], "key"))
+		return learner_key_statement_parse(block_mask,
+						   tokens,
+						   n_tokens,
+						   n_lines,
+						   err_line,
+						   err_msg);
+
+	if (!strcmp(tokens[0], "actions"))
+		return learner_actions_statement_parse(block_mask,
+						       tokens,
+						       n_tokens,
+						       n_lines,
+						       err_line,
+						       err_msg);
+
+	if (!strcmp(tokens[0], "default_action")) {
+		if (((n_tokens != 4) && (n_tokens != 5)) ||
+		    strcmp(tokens[2], "args") ||
+		    strcmp(tokens[3], "none") ||
+		    ((n_tokens == 5) && strcmp(tokens[4], "const"))) {
+			if (err_line)
+				*err_line = n_lines;
+			if (err_msg)
+				*err_msg = "Invalid default_action statement.";
+			return -EINVAL;
+		}
+
+		if (s->params.default_action_name) {
+			if (err_line)
+				*err_line = n_lines;
+			if (err_msg)
+				*err_msg = "Duplicate default_action stmt.";
+			return -EINVAL;
+		}
+
+		s->params.default_action_name = strdup(tokens[1]);
+		if (!s->params.default_action_name) {
+			if (err_line)
+				*err_line = n_lines;
+			if (err_msg)
+				*err_msg = "Memory allocation failed.";
+			return -ENOMEM;
+		}
+
+		if (n_tokens == 5)
+			s->params.default_action_is_const = 1;
+
+		return 0;
+	}
+
+	if (!strcmp(tokens[0], "size")) {
+		char *p = tokens[1];
+
+		if (n_tokens != 2) {
+			if (err_line)
+				*err_line = n_lines;
+			if (err_msg)
+				*err_msg = "Invalid size statement.";
+			return -EINVAL;
+		}
+
+		s->size = strtoul(p, &p, 0);
+		if (p[0]) {
+			if (err_line)
+				*err_line = n_lines;
+			if (err_msg)
+				*err_msg = "Invalid size argument.";
+			return -EINVAL;
+		}
+
+		return 0;
+	}
+
+	if (!strcmp(tokens[0], "timeout")) {
+		char *p = tokens[1];
+
+		if (n_tokens != 2) {
+			if (err_line)
+				*err_line = n_lines;
+			if (err_msg)
+				*err_msg = "Invalid timeout statement.";
+			return -EINVAL;
+		}
+
+		s->timeout = strtoul(p, &p, 0);
+		if (p[0]) {
+			if (err_line)
+				*err_line = n_lines;
+			if (err_msg)
+				*err_msg = "Invalid timeout argument.";
+			return -EINVAL;
+		}
+
+		return 0;
+	}
+
+	/* Anything else. */
+	if (err_line)
+		*err_line = n_lines;
+	if (err_msg)
+		*err_msg = "Invalid statement.";
+	return -EINVAL;
+}
+
 /*
  * regarray.
  *
@@ -1545,6 +1962,7 @@ rte_swx_pipeline_build_from_spec(struct rte_swx_pipeline *p,
 	struct action_spec action_spec = {0};
 	struct table_spec table_spec = {0};
 	struct selector_spec selector_spec = {0};
+	struct learner_spec learner_spec = {0};
 	struct regarray_spec regarray_spec = {0};
 	struct metarray_spec metarray_spec = {0};
 	struct apply_spec apply_spec = {0};
@@ -1761,6 +2179,40 @@ rte_swx_pipeline_build_from_spec(struct rte_swx_pipeline *p,
 			continue;
 		}
 
+		/* learner block. */
+		if (block_mask & (1 << LEARNER_BLOCK)) {
+			status = learner_block_parse(&learner_spec,
+						     &block_mask,
+						     tokens,
+						     n_tokens,
+						     n_lines,
+						     err_line,
+						     err_msg);
+			if (status)
+				goto error;
+
+			if (block_mask & (1 << LEARNER_BLOCK))
+				continue;
+
+			/* End of block. */
+			status = rte_swx_pipeline_learner_config(p,
+				learner_spec.name,
+				&learner_spec.params,
+				learner_spec.size,
+				learner_spec.timeout);
+			if (status) {
+				if (err_line)
+					*err_line = n_lines;
+				if (err_msg)
+					*err_msg = "Learner table configuration error.";
+				goto error;
+			}
+
+			learner_spec_free(&learner_spec);
+
+			continue;
+		}
+
 		/* apply block. */
 		if (block_mask & (1 << APPLY_BLOCK)) {
 			status = apply_block_parse(&apply_spec,
@@ -1934,6 +2386,21 @@ rte_swx_pipeline_build_from_spec(struct rte_swx_pipeline *p,
 			continue;
 		}
 
+		/* learner. */
+		if (!strcmp(tokens[0], "learner")) {
+			status = learner_statement_parse(&learner_spec,
+							 &block_mask,
+							 tokens,
+							 n_tokens,
+							 n_lines,
+							 err_line,
+							 err_msg);
+			if (status)
+				goto error;
+
+			continue;
+		}
+
 		/* regarray. */
 		if (!strcmp(tokens[0], "regarray")) {
 			status = regarray_statement_parse(&regarray_spec,
@@ -2042,6 +2509,7 @@ rte_swx_pipeline_build_from_spec(struct rte_swx_pipeline *p,
 	action_spec_free(&action_spec);
 	table_spec_free(&table_spec);
 	selector_spec_free(&selector_spec);
+	learner_spec_free(&learner_spec);
 	regarray_spec_free(&regarray_spec);
 	metarray_spec_free(&metarray_spec);
 	apply_spec_free(&apply_spec);
diff --git a/lib/pipeline/version.map b/lib/pipeline/version.map
index ff0974c2ee..c92d7d11cb 100644
--- a/lib/pipeline/version.map
+++ b/lib/pipeline/version.map
@@ -129,4 +129,12 @@ EXPERIMENTAL {
 	rte_swx_ctl_selector_field_info_get;
 	rte_swx_ctl_selector_group_id_field_info_get;
 	rte_swx_ctl_selector_member_id_field_info_get;
+
+	#added in 21.11
+	rte_swx_ctl_pipeline_learner_default_entry_add;
+	rte_swx_ctl_pipeline_learner_stats_read;
+	rte_swx_ctl_learner_action_info_get;
+	rte_swx_ctl_learner_info_get;
+	rte_swx_ctl_learner_match_field_info_get;
+	rte_swx_pipeline_learner_config;
 };
